<?php

define('ZEBRA_FORM_UPLOAD_RANDOM_NAMES', false);

/**
 *  Zebra_Form, a jQuery augmented PHP library for creating and validating HTML forms
 *
 *  It provides an easy and intuitive way of creating template-driven, visually appealing forms, complex client-side and
 *  server-side validations and prevention against cross-site scripting (XSS) and cross-site request forgery (CSRF) attacks
 *  prevention.
 *
 *  For the form validation part you can use the built-in rules (i.e. required fields, emails, minimum/maximum length,
 *  etc) and you can also define custom rules, with extreme ease, depending on your specific needs.
 *
 *  All the basic controls that you would find in a form are available plus a few extra: text, textarea, submit, image,
 *  reset, button, file, password, radio buttons, checkboxes, hidden, captcha, date and time pickers.
 *
 *  One additional note: this class is not a drag and drop utility - it is intended for coders who are comfortable with
 *  PHP, HTML, CSS and JavaScript/jQuery - you will have to build your forms when using this class, but it saves a great
 *  deal of time when it comes to validation and assures that your forms are secure and have a consistent look and feel
 *  throughout your projects!
 *
 *  Requires PHP 4.3.0+ or PHP 5.3+ (compiled with the php_fileinfo extension), and jQuery 1.6.2+
 *
 *  Visit {@link http://stefangabos.ro/php-libraries/zebra-form/} for more information.
 *
 *  For more resources visit {@link http://stefangabos.ro/}
 *
 *  @author     Stefan Gabos <contact@stefangabos.ro>
 *  @version    2.8.6 (last revision: August 13, 2012)
 *  @copyright  (c) 2006 - 2012 Stefan Gabos
 *  @license    http://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE
 *  @package    Zebra_Form
 */

class Zebra_Form
{

    /**
     *  Array containing all the controls added to the form
     *
     *  @var    array
     *
     *  @access private
     */
    var $controls;

    /**
     *  Array containing all the error messages generated by the form
     *
     *  @var    array
     *
     *  @access private
     */
    var $errors;

    /**
     *  An associative array of items uploaded to the current script via the HTTP POST method.
     *  This property, available only if a file upload has occurred, will have the same values as
     *  {@link http://php.net/manual/en/reserved.variables.files.php $_FILES} plus some extra values:
     *
     *  - <b>path</b>       -   the path where the file was uploaded to
     *  - <b>file_name</b>  -   the name the file was uploaded with
     *  - <b>imageinfo</b>  -   <b>available only if the uploaded file is an image!</b><br>
     *                          an array of attributes specific to the uploaded image as returned by
     *                          {@link http://www.php.net/manual/en/function.getimagesize.php getimagesize()} but
     *                          with meaningful names:<br>
     *                          <b>bits</b><br>
     *                          <b>channels</b><br>
     *                          <b>mime</b><br>
     *                          <b>width</b><br>
     *                          <b>height</b><br>
     *                          <b>type</b> ({@link http://php.net/manual/en/function.exif-imagetype.php possible types})<br>
     *                          <b>html</b><br>
     *
     *  <b>Note that the file name can be different than the original name of the uploaded file!</b>
     *
     *  By design, the script will append
     *  a number to the end of a file's name if at the path where the file is uploaded to there is another file with the
     *  same name (for example, if at the path where a file named "example.txt" is uploaded to, a file with the same name
     *  exists, the file's new name will be "example1.txt").
     *
     *  The file names can also be random-generated. See the {@link Zebra_Form_Control::set_rule() set_rule()} method and
     *  the <b>upload</b> rule
     *
     *  @var    array
     */
    var $file_upload;

    /**
     *  Indicates the {@link http://en.wikipedia.org/wiki/Filesystem_permissions filesystem} permissions to be set for 
     *  files uploaded through the {@link Zebra_Form_Control::set_rule() upload} rule.
     *
     *  <code>
     *  $form->file_upload_permissions = '0777';
     *  </code>
     *
     *  The permissions are set using PHP's {@link http://php.net/manual/en/function.chmod.php chmod} function which may
     *  or may not be available or be disabled on your environment. If so, this action will fail silently (no errors or
     *  notices will be shown by the library).
     *
     *  Better to leave this setting as it is.
     *
     *  If you know what you are doing, here is how you can calculate the permission levels:
     *
     *  - 400 Owner Read
     *  - 200 Owner Write
     *  - 100 Owner Execute
     *  - 40 Group Read
     *  - 20 Group Write
     *  - 10 Group Execute
     *  - 4 Global Read
     *  - 2 Global Write
     *  - 1 Global Execute
     *
     *  Default is '0755'
     *
     *  @var    string
     */
    var $file_upload_permissions;

    /**
     *  Array containing the variables to be made available in the template file (added through the {@link assign()}
     *  method)
     *
     *  @var    array
     *
     *  @access private
     */
    var $variables;

    /**
     *  Constructor of the class
     *
     *  Initializes the form.
     *
     *  <code>
     *  $form = new Zebra_Form('myform');
     *  </code>
     *
     *  @param  string  $name           Name of the form
     *
     *  @param  string  $method         (Optional) Specifies which HTTP method will be used to submit the form data set.
     *
     *                                  Possible (case-insensitive) values are <b>POST</b> an <b>GET</b>
     *
     *                                  Default is <b>POST</b>
     *
     *  @param  string  $action         (Optional) An URI to where to submit the form data set.
     *
     *                                  If left empty, the form will submit to itself.
     *
     *  @param  array   $attributes     (Optional) An array of attributes valid for a <form> tag (i.e. style)
     *
     *                                  Note that the following attributes are automatically set when the control is
     *                                  created and should not be altered manually:
     *
     *                                  <b>action</b>, <b>method</b>, <b>enctype</b>, <b>name</b>
     *
     *  @return void
     */
    function Zebra_Form($name, $method = 'POST', $action = '', $attributes = '')
    {

        $this->controls = $this->variables = $this->errors = $this->master_labels = array();

        // default filesysyem permissions for uploaded files
        $this->file_upload_permissions = '0755';

        // default values for the form's properties
        $this->form_properties = array(

            'action'                    =>  ($action == '' ? $_SERVER['REQUEST_URI'] : $action),
            'attributes'                =>  $attributes,
            'csrf_cookie_config'        =>  array('path' => '/', 'domain' => '', 'secure' => false, 'httponly' => false),
            'csrf_cookie_name'          =>  'zebra_csrf_token_' . $name,
            'csrf_storage_method'       =>  'auto',
            'csrf_token'                =>  '',
            'csrf_token_lifetime'       =>  0,
            'csrf_token_name'           =>  'zebra_csrf_token_' . $name,
            'doctype'                   =>  'html',
            'has_upload'                =>  false,
            'honeypot'                  =>  'zebra_honeypot_' . $name,
            'identifier'                =>  'name_' . $name,
            'language'                  =>  array(),
            'method'                    =>  strtoupper($method),
            'name'                      =>  $name,
            'other_suffix'              =>  '_other',
            'assets_server_path'        =>  rtrim(dirname(__FILE__), '\\/') . DIRECTORY_SEPARATOR,
            'assets_url'                =>  rtrim(str_replace('\\', '/', 'http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . rtrim($_SERVER['HTTP_HOST'], '\\/') . '/' . substr(rtrim(dirname(__FILE__), '\\/'), strlen($_SERVER['DOCUMENT_ROOT']))), '\\/') . '/',
            'show_all_error_messages'   =>  false,

        );

        // set default client-side validation properties
        $this->client_side_validation(true);

        // get the maximum allowed file size for uploads
        $upload_max_filesize = ini_get('upload_max_filesize');

        // see what it is given in (G, M, K)
        $unit = strtolower(substr($upload_max_filesize, -1));

        // get the numeric value
        $value = substr($upload_max_filesize, 0, -1);

        // convert to bytes
        // notice that there is no break
        switch (strtolower(substr($upload_max_filesize, -1))) {

            case 'g':
                $value*=1024;

            case 'm':
                $value*=1024;

            case 'k':
                $value*=1024;

        }

        // set the form's respective property
        $this->form_properties['max_file_size'] = $value;

        // save the version of PHP as we will use it in some instances to determine how to solve things
        $this->form_properties['php_version'] = str_pad(str_replace('.', '', phpversion()), 5, '0', STR_PAD_RIGHT);

        // include the XSS filter class - the Zebra_Form_Control class extends this class
        require_once dirname(__FILE__) . '/includes/XSSClean.php';

        // include the Control.php file which contains the Zebra_Form_Control class which is
        // extended by all of the classes
        require_once dirname(__FILE__) . '/includes/Control.php';

        // load the default language file
        $this->language('english');

    }

    /**
     *  Adds a control to the form.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // add a text control to the form
     *  // the "&" symbol is there so that $obj will be a reference to the object in PHP 4
     *  // for PHP 5+ there is no need for it
     *  $obj = &$form->add('text', 'my_text');
     *
     *  // make the text field required
     *  $obj->set_rule(
     *       'required' => array(
     *          'error',            // variable to add the error message to
     *          'Field is required' // error message if value doesn't validate
     *       )
     *  );
     *
     *  // don't forget to always call this method before rendering the form
     *  if ($form->validate()) {
     *      // put code here
     *  }
     *
     *  //  output the form using an automatically generated template
     *  $form->render();
     *  </code>
     *
     *  @param  string  $type   Type of the control to add.
     *
     *                          Controls that can be added to a form:
     *
     *                          -   {@link Zebra_Form_Button buttons}
     *                          -   {@link Zebra_Form_Captcha CAPTCHAs}
     *                          -   {@link Zebra_Form_Checkbox checkboxes}
     *                          -   {@link Zebra_Form_Date date pickers}
     *                          -   {@link Zebra_Form_File file upload controls}
     *                          -   {@link Zebra_Form_Hidden hidden controls}
     *                          -   {@link Zebra_Form_Image image button controls}
     *                          -   {@link Zebra_Form_Label labels}
     *                          -   {@link Zebra_Form_Note notes}
     *                          -   {@link Zebra_Form_Password password controls}
     *                          -   {@link Zebra_Form_Radio radio buttons}
     *                          -   {@link Zebra_Form_Reset reset buttons}
     *                          -   {@link Zebra_Form_Select selects}
     *                          -   {@link Zebra_Form_Submit submit buttons}
     *                          -   {@link Zebra_Form_Text text box controls}
     *                          -   {@link Zebra_Form_Textarea textareas}
     *                          -   {@link Zebra_Form_Time time pickers}
     *
     *  @param  mixed   $arguments  A list of arguments as required by the control that is added.
     *
     *  @return reference           Returns a reference to the newly created object
     */
    function &add($type)
    {

        // if shortcut for multiple radio buttons or checkboxes
        if ($type == 'radios' || $type == 'checkboxes') {

            // if there are less than 3 arguments
            if (func_num_args() < 3)

                // trigger a warning
                _zebra_form_show_error('For <strong>' . $type . '</strong>, the <strong>add()</strong> method requires at least 3 arguments', E_USER_WARNING);

            // if third argument is not an array
            elseif (!is_array(func_get_arg(2)))

                // trigger a warning
                _zebra_form_show_error('For <strong>' . $type . '</strong>, the <strong>add()</strong> method requires the 3rd argument to be an array', E_USER_WARNING);

            // if everything is ok
            else {

                // controls' name
                $name = func_get_arg(1);

                // the values and labels
                $values = func_get_arg(2);

                // a 4th argument (the default option) was passed to the method
                if (func_num_args() >= 4) {

                    // save the default value
                    $default = func_get_arg(3);

                    // if default value is not given as an array
                    // (makes sense for checkboxes when there may be multiple preselected values)
                    // make it an array
                    if (!is_array($default)) $default = array($default);

                }

                $counter = 0;

                // iterate through values and their respective labels
                foreach ($values as $value => $caption) {

                    // create control
                    $obj = & $this->add(($type == 'radios' ? 'radio' : 'checkbox'), $name, $value, (isset($default) && in_array($value, $default) ? array('checked' => 'checked') : ''));

                    // if this is the first control in the array
                    // we will later need to return a reference to it
                    if ($counter++ == 0) $pointer = &$obj;

                    // sanitize controls' name (remove square brackets)
                    $sanitize_name = preg_replace('/\[\]$/', '', $name);

                    // add the label for the control
                    $this->add('label', 'label_' . $sanitize_name . '_' . $value, $sanitize_name . '_' . $value, $caption);

                }

                // if the array of values was not empty
                // return reference to the first control
                if (isset($pointer)) return $pointer;

            }

        // for all other controls
        } else {

            $file_name = ucfirst(strtolower($type));

            // the classes have the "Zebra_Form_" prefix
            $class_name = 'Zebra_Form_' . ucfirst(strtolower($type));

            // include the file containing the PHP class, if not already included
            require_once dirname(__FILE__) . '/includes/' . $file_name . '.php';

            // if included file contains such a class
            if (class_exists($class_name)) {

                // if PHP version is lower than 5
                if (substr(phpversion(), 0, 1) < 5) {

                    // convert arguments passed to the add() method to a string ready to be parsed by eval()
                    // notice that first argument is ignored as it refers to the type of the control to add
                    // and we don't have to pass that to the class
                    $arguments = '';

                    // iterates through the arguments, ignoring the very first one
                    for ($i = 1; $i < func_num_args(); $i++)

                        // and creates the argument list
                        $arguments .= ($arguments != '' ? ',' : '') . var_export(func_get_arg($i), true);

                    // use this method to create the new object with the given arguments
                    eval("\$obj = & new \$class_name(" . $arguments . ");");

                // if PHP version is at least 5
                } else {

                    // prepare arguments passed to the add() method
                    // notice that first argument is ignored as it refers to the type of the control to add
                    // and we don't have to pass that to the class
                    $arguments = array_slice(func_get_args(), 1);

                    // use this method to instantiate the object with dynamic arguments
                    $obj = call_user_func_array(array(new ReflectionClass($class_name), 'newInstance'), $arguments);

                }

                // make available the form's properties in the newly created object
                $obj->form_properties = & $this->form_properties;

                // get some attributes for the newly created control
                $attributes = $obj->get_attributes(array('id', 'name'));

                // perform some extra tasks for different types of controls
                switch ($class_name) {

                    // if the newly created control is a file upload control
                    case 'Zebra_Form_File':

                        // set a flag to be used at rendering
                        $this->form_properties['has_upload'] = true;

                        break;

                    // if the newly created control is a radio button or a checkbox
                    case 'Zebra_Form_Radio':
                    case 'Zebra_Form_Checkbox':

                        // radio buttons and checkboxes might have a "master label", a label that is tied to the radio buttons' or
                        // checkboxes' name rather than individual controls' IDs. (as grouped radio buttons and checkboxes share
                        // the same name but have different values)
                        // we use this so that, if the controls have the "required" rule set, the asterisk is attached to the master
                        // label rather than to one of the actual controls

                        // therefore, we generate a "lookup" array of "master" labels for each group of radio buttons or
                        // checkboxes. this does not means that there will be an actual master label - we use this lookup
                        // array to easily determine if a master label exists when rendering the form

                        // sanitize the control's name
                        $attributes['name'] = preg_replace('/\[\]$/', '', $attributes['name']);

                        // if there isn't a master label for the group the current control is part of
                        if (!isset($this->master_labels[$attributes['name']]))

                            // create the entry
                            // the "control" index will hold the actual label's name if a "master" label is added to the form
                            $this->master_labels[$attributes['name']] = array('control' => false);

                        break;

                }

                // put the reference to the newly created object in the 'controls' array
                $this->controls[$attributes['id']] = &$obj;

                // return the identifier to the newly created object
                return $obj;

            }

        }

    }

    /**
     *  Appends a message to an already existing {@link Zebra_Form_Control::set_rule() error block}
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // add a text control to the form
     *  // the "&" symbol is there so that $obj will be a reference to the object in PHP 4
     *  // for PHP 5+ there is no need for it
     *  $obj = &$form->add('text', 'my_text');
     *
     *  // make the text field required
     *  $obj->set_rule(
     *       'required' => array(
     *          'error',            // variable to add the error message to
     *          'Field is required' // error message if value doesn't validate
     *       )
     *  );
     *
     *  // don't forget to always call this method before rendering the form
     *  if ($form->validate()) {
     *
     *      // for the purpose of this example, we will do a custom validation
     *      // after calling the "validate" method.
     *      // for custom validations, using the "custom" rule is recommended instead
     *
     *      // check if value's is between 1 and 10
     *      if ((int)$_POST['my_text']) < 1 || (int)$_POST['my_text']) > 10) {
     *
     *          $form->add_error('error', 'Value must be an integer between 1 and 10!');
     *
     *      } else {
     *
     *          // put code here that is to be executed when the form values are ok
     *
     *      }
     *
     *  }
     *
     *  //  output the form using an automatically generated template
     *  $form->render();
     *  </code>
     *
     *  @param  string  $error_block    The name of the error block to append the error message to (also the name
     *                                  of the PHP variable that will be available in the template file).
     *
     *  @param  string  $error_message  The error message to append to the error block.
     *
     *  @return void
     */
    function add_error($error_block, $error_message)
    {

        // if the error block was not yet created, create the error block
        if (!isset($this->errors[$error_block])) $this->errors[$error_block] = array();

        // if the same exact message doesn't already exists
        if (!in_array(trim($error_message), $this->errors[$error_block]))

            // append the error message to the error block
            $this->errors[$error_block][] = trim($error_message);

    }

    /**
     *  Set the server path and URL to the "process.php" and "mimes.json" files.
     *
     *  These files are required for CAPTCHAs and uploads.
     *
     *  By default, the location of these files is in the same folder as Zebra_Form.php and the script will automatically
     *  try to determine both the server path and the URL to these files. However, when the script is run on a virtual
     *  host the script may not correctly determine the paths to these files. In these instances, use this method to
     *  correctly set the server path - needed by the script to correctly include these files, and the URL - needed by
     *  the client-side validation to include these files.
     *
     *  Also, for security reasons, I recommend moving these two files by default to the root of your website (or another
     *  publicly accessible place) and manually set the paths, in order to prevent malicious users from finding out 
     *  information about your directory structure.
     *
     *  @param  string  $server_path    The server path to "process.php" and "mimes.json" files.
     *
     *  @param  string  $url            The URL to "process.php" and "mimes.json" files.
     *
     *  @return void
     */
    function assets_path($server_path, $url)
    {

        // set values
        $this->form_properties['assets_server_path'] = $server_path;
        $this->form_properties['assets_url'] = $url;

    }

    /**
     *  Creates a PHP variable with the given value, available in the template file.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // make available the $my_value variable in the template file
     *  $form->assign('my_value', '100');
     *
     *  // don't forget to always call this method before rendering the form
     *  if ($form->validate()) {
     *      // put code here
     *  }
     *
     *  // output the form
     *  // notice that we are using a custom template
     *  // my_template.php file is expected to be found
     *  // and in this file, you may now use the $my_value variable
     *  $form->render('my_template.php');
     *  </code>
     *
     *  @param  string  $variable_name  Name by which the variable will be available in the template file.
     *
     *  @param  mixed   $value          The value to be assigned to the variable.
     *
     *  @return void
     */
    function assign($variable_name, $value)
    {

        // save the variable in an array that we will make available in the template file upon rendering
        $this->variables[$variable_name] = $value;

    }

    /**
     *  Sets properties for the client-side validation.
     *
     *  Client-side validation, when enabled, occurs on the "onsubmit" event of the form.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  // disable client-side validation
     *  $form->client_side_validation(false);
     *
     *  // enable client-side validation using default properties
     *  $form->client_side_validation(true);
     *
     *  // enable client-side validation using customized properties
     *  $form->client_side_validation(array(
     *      'scroll_to_error'       =>  false,      //  don't scroll the browser window to the error message
     *      'tips_position'         =>  'right',    //  position tips with error messages to the right of the controls
     *      'close_tips'            =>  false,      //  don't show a "close" button on tips with error messages
     *      'validate_on_the_fly'   =>  false,      //  don't validate controls on the fly
     *      'validate_all'          =>  false,      //  show error messages one by one upon trying to submit an invalid form
     *  ));
     *  </code>
     *
     *  To access the JavaScript object and use the public methods provided by it, use $('#formname').data('Zebra_Form')
     *  where <i>formname</i> is the form's name <b>with any dashes turned into underscores!</b>
     *
     *  <i>Therefore, if a form's name is "my-form", the JavaScript object would be accessed like $('my_form').data('Zebra_Form').</i>
     *
     *  From JavaScript, these are the methods that can be called on this object:
     *
     *  -   <b>attach_tip(element, message)</b> -   displays an error message attached to the indicated jQuery element;
     *  -   <b>hide_errors()</b>                -   hides all error message tips;
     *  -   <b>show_errors()</b>                -   shows the error message/messages where required, if the form is not validated
     *  -   <b>submit()</b>                     -   submits the form;
     *  -   <b>validate()</b>                   -   checks if the form is valid; returns TRUE or FALSE;
     *
     *  Here's how you can use these methods, in a JavaScript file:
     *
     *  <i>Note that this is just to show how to use each of the methods presented above, and is not the recommended way
     *  of doing custom validations! Refer to the{@link Zebra_Form_Control::set_rule() set_rule()} method for how to do
     *  custom validations and for a complete example on how to also use AJAX (scroll down to the "custom" rule).</i>
     *
     *  <code>
     *  //  let's submit the form when clicking on a random button
     *
     *  // get a reference to the Zebra_Form object
     *  var $form = $('#formname').data('Zebra_Form');
     *
     *  // handle the onclick event on a random button
     *  $('#somebutton').bind('click', function(e) {
     *
     *      // stop default action
     *      e.preventDefault();
     *
     *      // hide all error messages that might still be visible
     *      $form.hide_errors();
     *
     *      // validate the form, and if the form validates
     *      if ($form.validate()) {
     *
     *          // do your own thing here
     *
     *          // maybe do a custom check, and attach an error message to an element
     *          // on the form, if something is not right
     *          if (check_something_here) $form.attach_tip($('#form_element'), 'Error!');
     *
     *          // if everything is ok even after the custom validation, submit the form
     *          else $form.submit();
     *
     *      // if the form is not valid
     *      // show error message/messages where required
     *      } else $form.show_errors();
     *
     *  });
     *  </code>
     *
     *  @param  mixed   $properties     Can have the following values:
     *                                  -   FALSE, disabling the client-side validation;
     *                                  -   TRUE, enabling the client-side validation with the default properties;
     *                                  -   an associative array with customized properties for the client-side validation;
     *
     *                                  In this last case, the available properties are:
     *
     *                                  -   <b>scroll_to_error</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether the browser window should be scrolled to the error message
     *                                      or not.<br>
     *                                      Default is <b>TRUE</b>.
     *                                  -   <b>tips_position</b>, string, <i>left</i> or <i>right</i><br>
     *                                      Specifies where the error message tip should be positioned relative to the
     *                                      control.<br>
     *                                      Default is <b>left</b>.
     *                                  -   <b>close_tips</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether the tips with error messages should have a "close" button
     *                                      or not<br>
     *                                      Default is <b>TRUE</b>.
     *                                  -   <b>validate_on_the_fly</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether values should be validated as soon as the user leaves a
     *                                      field; if set to TRUE and the validation of the control fails, the error
     *                                      message will be shown right away<br>
     *                                      Default is <b>FALSE</b>.
     *                                  -   <b>validate_all</b>, boolean, TRUE or FALSE<br>
     *                                      Specifies whether upon submitting the form, should all error messages be
     *                                      shown at once if there are any errors<br>
     *                                      Default is <b>FALSE</b>.
     *
     *  @return void
     */
    function client_side_validation($properties)
    {

        // default properties of the client-side validation
        $defaults = array(
            'scroll_to_error'       =>  true,
            'tips_position'         =>  'left',
            'close_tips'            =>  true,
            'validate_on_the_fly'   =>  false,
            'validate_all'          =>  false,
        );

        // if client-side validation needs to be disabled
        if ($properties === false)

            $this->form_properties['clientside_validation'] = false;

        // if client-side validation needs to be enabled with default properties
        elseif ($properties === true)

            $this->form_properties['clientside_validation'] = $defaults;

        // if custom settings for client-side validation
        elseif (is_array($properties))

            // if current settings are already stored as an array
            if (is_array($this->form_properties['clientside_validation']))

                // merge the new settings with the old ones
                $this->form_properties['clientside_validation'] = array_merge($this->form_properties['clientside_validation'], $properties);

            // if current settings are not currently an array
            else

                // the new settings will overwrite the previous one
                $this->form_properties['clientside_validation'] = $properties;

    }

    /**
     *  By default, this class generates <b>HTML 4.01 Strict</b> markup.
     *
     *  Use this method if you want the generated HTML markup to validate as <b>XHTML 1.0 Strict</b>.
     *
     *  @param  string  $doctype    (Optional) The DOCTYPE of the generated HTML markup.
     *
     *                              Possible (case-insensitive) values are <b>HTML</b> or <b>XHTML</b>
     *
     *                              Default is HTML.
     *
     *  @return void
     */
    function doctype($doctype = 'html')
    {

        // set the doctype
        $this->form_properties['doctype'] = (strtolower($doctype) == 'xhtml' ? 'xhtml' : 'html');

    }

    /**
     *  Enables protection against {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery Cross-site request
     *  forgery} (CSRF) attacks.
     *
     *  Read more about specifics and a simple implementation on
     *  {@link http://shiflett.org/articles/cross-site-request-forgeries Chris Shiflett's website}.
     *
     *  This method is automatically called by the library - unless you call it yourself *before* the form's {@link validate()}
     *  and {@link render()} methods, so protection against CSRF attacks is enabled by default for all forms and the
     *  script will decide automatically on the method to use for storing the CSRF token: if a session is already started
     *  then the CSRF token will be stored in a session variable or, if a session is not started, the CSRF token will be
     *  stored in a session cookie (cookies that expire when the browser is closed) but, in this case, it offers a lower
     *  level of security.
     *
     *  <i>You are encouraged to start a PHP session before instantiating this class in order to maximize the level of
     *  security of your forms.</i>
     *
     *  The CSRF token is automatically regenerated when the form is submitted regardless if the form validated or not.
     *  A notable exception is that the form doesn't validate but was submitted via AJAX the CSRF token will not be
     *  regenerated - useful if you submit forms by AJAX.
     *
     *  As an added benefit, protection against CSRF attacks prevents "double posts" by design.
     *
     *  <code>
     *  // recommended usage is:
     *
     *  // call session_start() somewhere in your code but before outputting anything to the browser
     *  session_start();
     *
     *  // include the Zebra_Form
     *  require 'path/to/Zebra_Form.php';
     *
     *  // instantiate the class
     *  // protection against CSRF attack will be automatically enabled
     *  // but will be less secure if a session is not started (as it will
     *  // rely on cookies)
     *  $form = new Zebra_Form('my_form');
     *  </code>
     *
     *  @param  integer $csrf_token_lifetime    (Optional) The number of seconds after which the CSRF token is to be
     *                                          considered as expired.
     *
     *                                          If set to "0" the tokens will expire at the end of the session (when the
     *                                          browser closes or session expires).
     *
     *                                          <i>Note that if csrf_storage_method is set to "session" this value cannot
     *                                          be higher than the session's life time as, if idle, the session will time
     *                                          out regardless of this value!</i>
     *
     *                                          Default is 0.
     *
     *  @param  string  $csrf_storage_method    (Optional) Sets whether the CSRF token should be stored in a cookie, in
     *                                          a session variable, or let the script to automatically decide and use
     *                                          sessions if available or a cookie otherwise.
     *
     *                                          Possible values are "auto", "cookie", "session" or boolean FALSE.
     *
     *                                          If value is "auto", the script will decide automatically on what to use:
     *                                          if a session is already started then the CSRF token will be stored in a
     *                                          session variable, or, if a session is not started, the CSRF token will be
     *                                          stored in a cookie with the parameters as specified by the
     *                                          <b>csrf_cookie_config</b> argument (read below).
     *
     *                                          If value is "cookie" the CSRF token will be stored in a cookie with the
     *                                          parameters as specified by the <b>csrf_cookie_config</b> argument (read
     *                                          below).
     *
     *                                          If value is "session" the CSRF token will be stored in a session variable
     *                                          and thus a session must be started before instantiating the library.
     *
     *                                          If value is boolean FALSE (not recommended), protection against CSRF
     *                                          attack will be disabled.
     *
     *                                          The stored value will be compared, upon for submission, with the value
     *                                          stored in the associated hidden field, and if the two values do not match
     *                                          the form will not validate.
     *
     *                                          Default is "auto".
     *
     *  @param  array   $csrf_cookie_config     (Optional) An associative array containing the properties to be used when
     *                                          setting the cookie with the CSRF token (if <b>csrf_storage_method</b> is
     *                                          set to "cookie").
     *
     *                                          The properties that can be set are "path", "domain", "secure" and "httponly".
     *                                          where:
     *
     *                                          -   <b>path</b>     -   the path on the server in which the cookie will
     *                                                                  be available on. If set to "/", the cookie will
     *                                                                  be available within the entire domain. If set to
     *                                                                  '/foo/', the cookie will only be available within
     *                                                                  the /foo/ directory and all subdirectories such
     *                                                                  as /foo/bar/ of domain.<br>
     *                                                                  Default is "/"
     *
     *                                          -   <b>domain</b>   -   The domain that the cookie will be available on.
     *                                                                  To make the cookie available on all subdomains of
     *                                                                  example.com, domain should be set to to
     *                                                                  ".example.com". The . (dot) is not required but
     *                                                                  makes it compatible with more browsers. Setting
     *                                                                  it to "www.example.com" will make the cookie
     *                                                                  available only in the www subdomain.
     *
     *                                          -   <b>secure</b>   -   Indicates whether cookie information should only
     *                                                                  be transmitted over a HTTPS connection.<br>
     *                                                                  Default is FALSE.
     *
     *                                          -   <b>httponly</b> -   When set to TRUE the cookie will be made accessible
     *                                                                  only through the HTTP protocol. This means that
     *                                                                  the cookie won't be accessible by scripting languages,
     *                                                                  such as JavaScript. It has been suggested that
     *                                                                  this setting can effectively help to reduce identity
     *                                                                  theft through XSS attacks (although it is not
     *                                                                  supported by all browsers), but that claim is often
     *                                                                  disputed. Available only in PHP 5.2.0+<br>
     *                                                                  Default is FALSE
     *
     *                                          Not all properties must be set - for the properties that are not set, the
     *                                          default values will be used instead.
     *
     *  @since  2.8.4
     *
     *  @return void
     */
    function csrf($csrf_token_lifetime = 0, $csrf_storage_method = 'auto', $csrf_cookie_config = array('path' => '/', 'domain' => '', 'secure' => false, 'httponly' => false))
    {

        // continue only if protection against CSRF attacks is not disabled and a token was not already generated
        if ($this->form_properties['csrf_storage_method'] !== false && $this->form_properties['csrf_token'] == '') {

            // set the storage method for the CSRF token
            $this->form_properties['csrf_storage_method'] = ($csrf_storage_method === false ? false : strtolower(trim($csrf_storage_method)));

            // if protection against CSRF attacks is not disabled
            if ($this->form_properties['csrf_storage_method'] !== false) {

                // if the script should decide what method to use and a session is already started
                if ($this->form_properties['csrf_storage_method'] == 'auto')

                    // use sessions as storage method
                    if (isset($_SESSION)) $this->form_properties['csrf_storage_method'] = 'session';

                    // if a session is not already started, use cookies as storage method
                    else $this->form_properties['csrf_storage_method'] = 'cookie';

                // set the life time of the CSRF token
                $this->form_properties['csrf_token_lifetime'] = ($csrf_token_lifetime <= 0 ? 0 : $csrf_token_lifetime);

                // set the configuration options for cookies
                $this->form_properties['csrf_cookie_config'] = array_merge($this->form_properties['csrf_cookie_config'], $csrf_cookie_config);

                // generate a new CSRF token (if it is the case)
                $this->_csrf_generate_token();

            }

        }

    }

    /**
     *  Sets the language to be used by some of the form's controls (the date control, the select control, etc.)
     *
     *  The default language is English.
     *
     *  @param  string  $language   The name of the language file to be used, from the "languages" folder.
     *
     *                              Must be specified without extension ("german" for the german language
     *                              not "german.php")!
     *
     *  @var   string
     *
     *  @return void
     */
    function language($language)
    {

        // include the language file
        require rtrim(dirname(__FILE__), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'languages/' . strtolower(trim($language)) . '.php';

        // make the language available in the control
        $this->form_properties['language'] = &$this->language;

    }

    /**
     *  Renders the form.
     *
     *  @param  string  $template       The output of the form can be generated automatically, can be given from a template
     *                                  file or can be generated programmatically by a callback function.
     *
     *                                  For the automatically generated template there are two options:
     *
     *                                  -   when <i>$template</i> is an empty string or is "<i>*vertical</i>", the script
     *                                      will automatically generate an output where the labels are above the controls
     *                                      and controls come one under another (vertical view)
     *
     *                                  -   when <i>$template</i> is "<i>*horizontal</i>", the script will automatically
     *                                      generate an output where the labels are positioned to the left of the controls
     *                                      while the controls come one under another (horizontal view)
     *
     *                                  When templates are user-defined, <i>$template</i> needs to be a string representing
     *                                  the <i>path/to/the/template.php</i>.
     *
     *                                  The template file itself must be a plain PHP file where all the controls
     *                                  added to the form (except for the hidden controls, which are handled automatically)
     *                                  will be available as variables with the names as described in the documentation
     *                                  for each of the controls. Also, error messages will be available as described at
     *                                  {@link Zebra_Form_Control::set_rule() set_rule()}.
     *
     *                                  A special variable will also be available in the template file - a variable with
     *                                  the name of the form and being an associative array containing all the controls
     *                                  added to the form, as objects.
     *
     *                                  <i>The template file must not contain the <form> and </form> tags, nor any of the
     *                                  <hidden> controls added to the form as these are generated automatically!</i>
     *
     *                                  There is a third method of generating the output and that is programmatically,
     *                                  through a callback function. In this case <i>$template</i> needs to be the name
     *                                  of an existing function.
     *
     *                                  The function will be called with two arguments:
     *
     *                                  -   an associative array with the form's controls' ids and their respective
     *                                      generated HTML, ready for echo-ing (except for the hidden controls which will
     *                                      still be handled automatically);
     *                                  -   an associative array with all the controls added to the form, as objects
     *
     *                                  THE USER FUNCTION MUST RETURN THE GENERATED OUTPUT!
     *
     *  @param  boolean $return         (Optional) If set to TRUE, the output will be returned instead of being printed
     *                                  to the screen.
     *
     *                                  Default is FALSE.
     *
     *  @param  array   $variables      (Optional) An associative array in the form of "variable_name" => "value"
     *                                  representing variable names and their associated values, to be made available
     *                                  in custom template files.
     *
     *                                  This represents a quicker alternative for assigning many variables at once
     *                                  instead of calling the {@link assign()} method for each variable.
     *
     *  @return mixed                   Returns or displays the rendered form.
     */
    function render($template = '', $return = true, $variables = '')
    {

        // if
        if (

            // "process.php" file could not be found
            !file_exists($this->form_properties['assets_server_path'] . 'process.php') ||

            // or "mimes.json" file could not be found
            !file_exists($this->form_properties['assets_server_path'] . 'mimes.json')

        )

            // it means the most probably the script is run on a virtual host and that paths need to be set manually so
            // we inform the user about that
            _zebra_form_show_error('<strong>Zebra_Form</strong> could not automatically determine the correct path to the "process.php"
            and "mimes.json" files - this may happen if the script is run on a virtual host. To fix this, use the <u>assets_path()</u>
            method and manually set the correct <strong>server path</strong> and <strong>URL</strong> to these files ', E_USER_ERROR);

        // if variables is an array
        if (is_array($variables))

            // iterate through the values in the array
            foreach ($variables as $name => $value)

                // make each value available in the template
                $this->assign($name, $value);

        // start generating the output
        $output = '<form ' .
            ($this->form_properties['doctype'] == 'html' ? 'name="' . $this->form_properties['name'] . '" ' : '') .
            'id="' . $this->form_properties['name'] . '" ' .
            'action="' . htmlspecialchars($this->form_properties['action']) . '" ' .
            'method="' . strtolower($this->form_properties['method']) . '" ';

        // if custom classes are to be set for the form
        if (isset($this->form_properties['attributes']['class']))

            // add the "Zebra_Form" required class
            $this->form_properties['attributes']['class'] .= ' Zebra_Form';

        // if no custom classes are set, set the required "Zebra_Form" class
        else $this->form_properties['attributes']['class'] = 'Zebra_Form';

        // if any form attributes have been specified
        if (is_array($this->form_properties['attributes']))

            // iterate through the form's attributes
            foreach ($this->form_properties['attributes'] as $attribute => $value)

                // write them
                $output .= ' ' . $attribute . '="' . $value . '"';

        // if the form has file upload controls
        if ($this->form_properties['has_upload'] === true) {

            // add the enctype to the attributes of the <form> tag
            $output .= ' enctype="multipart/form-data"';

            // and add this required hidden field containing the maximum allowed file size
            $this->add('hidden', 'MAX_FILE_SIZE', $this->form_properties['max_file_size']);

            // if client-side validation is not disabled
            if ($this->form_properties['clientside_validation'] !== false)

                // add a new property for the client-side validation
                $this->client_side_validation(array('assets_path' => rawurlencode($this->form_properties['assets_url'])));

        }

        $output .= '>';

        // iterate through the form's controls
        foreach ($this->controls as $key => $control) {

            // get some attributes for each control
            $attributes = $control->get_attributes(array('type', 'for', 'name', 'id', 'multiple', 'other', 'class', 'default_other'));

            // sanitize the control's name
            $attributes['name'] = preg_replace('/\[\]/', '', $attributes['name']);

            // validate the control's name
            switch ($attributes['name']) {

                // if control has the same name as the form
                case $this->form_properties['name']:
                // if control has the same name as the name of the honeypot's name
                case $this->form_properties['honeypot']:
                // if control has the same name as the name of the field containing the CSRF token
                case $this->form_properties['csrf_token_name']:
                // if control has the name "submit"
                case 'submit':

                    // stop the execution of the script
                    _zebra_form_show_error('You are not allowed to have a control named "<strong>' .
                        $attributes['name'] . '</strong>" in form "<strong>' .
                        $this->form_properties['name'] . '</strong>"',
                    E_USER_ERROR);

                    break;

            }

            // if control name is not allowed because it looks like the automatically generated controls for <select> controls
            // with the "other" option attached
            if (preg_match('/' . preg_quote($this->form_properties['other_suffix']) . '$/', $attributes['name']) > 0)

                // stop the execution of the script
                _zebra_form_show_error('You are not allowed to have a control with the name ending in "<strong>' .
                    $this->form_properties['other_suffix'] . '</strong>" in form "<strong>' .
                    $this->form_properties['name'] . '</strong>"', E_USER_ERROR);

            // if client-side validation is enabled and control has any rules attached to it
            if (is_array($this->form_properties['clientside_validation']) && !empty($control->rules)) {

                // if variable not created yet, create the variable holding client-side error messages
                if (!isset($clientside_error_messages)) $clientside_error_messages = '';

                // add error message
                $clientside_error_messages .= ($clientside_error_messages != '' ? ',' : '') . '"' . $attributes['id'] . '":{';

                $class = $rules = '';

                // we need to make sure that rules are in propper order - "required" must always be checked first when
                // present and "upload" needs to precede any other upload-related rules;

                // if the upload rule exists
                if (isset($control->rules['upload'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('upload', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule
                    $control->rules = array_merge($rule, $control->rules);

                }

                // if the "requried" rule exists
                if (isset($control->rules['required'])) {

                    // remove it from wherever it is
                    $rule = array_splice($control->rules, array_search('required', array_keys($control->rules)), 1, array());

                    // and make sure it's the first rule (it has to checked prior to the "upload" rule)
                    $control->rules = array_merge($rule, $control->rules);

                }

                // iterate through the rules attached to the control
                foreach ($control->rules as $rule => $properties) {

                    // these rules are not checked client side
                    if ($rule == 'resize' || $rule == 'convert') continue;

                    // start preparing the class that is to be added to the control
                    $class .= ($class != '' ? ',' : '') . $rule;

                    // for some rules we perform some additional tasks
                    switch ($rule) {

                        case 'regexp':

                            if (trim($properties[0]) != '') {

                                // the class name also contains the regular expression
                                $class .= '(' . preg_replace(array('/\[/', '/\]/', '/\,/', '/\(/', '/\)/'), array('lsqb;', 'rsqb;', 'comma;', 'lsb;', 'rsb;'), $properties[0]) . ')';

                            }

                            break;

                        // for some of the rules
                        case 'alphabet':
                        case 'alphanumeric':
                        case 'compare':
                        case 'digits':
                        case 'filesize':
                        case 'filetype':
                        case 'float':
                        case 'number':

                            // the class name also contains the extra parameter(s)
                            $class .= '(' . preg_replace('/\,/', 'comma;', $properties[0]) . ')';

                            break;

                        // for the custom rule
                        case 'custom':

                            $messages = '';

                            // as custom rules are always given as an array
                            // iterate through the available custom rules
                            foreach ($properties as $counter => $values) {

                                // if custom function is given as a function created with create_function
                                if ((ord($values[0][0]) == 0 && preg_match('/.{1}lambda\_[0-9]+/', $values[0])))

                                    // function name is the control's id and the number of custom function as suffix
                                    $function_name = $attributes['id'] . ($counter + 1);

                                // if custom function is given as the name of a function
                                // the function's name is the given name
                                else $function_name = $values[0];

                                // generate the validation rules
                                $class .= ($counter > 0 ? ',' : '(') . $function_name . 'comma;';

                                // the custom arguments to be passed to the custom functions
                                $class .= implode('comma;', array_map(create_function('$value', 'return preg_replace(array("/\,/"), array("mark;"), $value);'), array_slice($values, 1, -2)));

                                // remove any trailing "comma;" that may have left over if there are no custom arguments to the function
                                $class = preg_replace('/comma\;.*?$/', '', $class);

                                // the error message
                                $messages .= ($counter > 0 ? ',' : '') . '"custom_' . $function_name . '":"' . $values[count($values) - 1] . '"';

                            }

                            // wrap up
                            $class .= ')';

                            break;

                        // for the date_compare rule
                        case 'datecompare':

                            // the class name also contains the control to compare the date to and the operator
                            $class .= '(' . $properties[0] . ',' . $properties[1] . ')';

                            break;

                        // for the length rule
                        case 'length':

                            // the class name also contain min/max
                            $class .= '(' . $properties[0] . ',' . $properties[1] . ')';

                            // if max is greater than 0
                            if ($properties[1] > 0)

                                // we also set the maxlength attribute of the control
                                $this->controls[$key]->set_attributes(array('maxlength' => $properties[1]));

                            break;

                        case 'upload':

                            // the class name also contains the extra parameter(s)
                            $class .= '(' . rawurlencode(trim('http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . $_SERVER['HTTP_HOST']. dirname($_SERVER['PHP_SELF']), '/') . '/' . $properties[0]) . ')';

                            break;

                    }

                    // if custom rule
                    if ($rule == 'custom')

                        // add the error message to the javascript object
                        $rules .= ($rules != '' ? ',' : '') . $messages;

                    // for other rules
                    // (except those which do not have equivalents in JavaScript)
                    else

                        // add the error message to the javascript object
                        $rules .= ($rules != '' ? ',' : '') . '"' . $rule . '":"' . addcslashes($properties[count($properties) - ($rule == 'length' && count($properties) == 5 ? 2 : 1)], '"') . '"';

                }

                // wrap up client-side error messages
                $clientside_error_messages .= $rules .= '}';

                // add a class so that the javascript validator knows that it has to validate the control
                $this->controls[$key]->set_attributes(array('class' => 'validate[' . $class . ']'), false);

            }

            // if control is a select control, doesn't have the "multiple" attribute set and has the "other" attribute set
            if (isset($attributes['type']) && $attributes['type'] == 'select' && !isset($attributes['multiple']) && isset($attributes['other'])) {

                // set a special class for the select control so that we know that it has a textbox attached to it

                // add a special class to the control
                $this->controls[$key]->set_attributes(array('class'=>'other'), false);

                // add a text control
                $obj = & $this->add('text', $attributes['id'] . $this->form_properties['other_suffix'], $attributes['default_other']);

                // set a special class for the control
                $obj->set_attributes(array('class'=>'other'), false);

                // if the select control was not submitted OR it was submitted but the selected option is other than
                // the "other" option
                if (!isset($control->submitted_value) || $control->submitted_value != 'other')

                    // hide the text box
                    $obj->set_attributes(array('class'=>'other-invisible'), false);

                // make sure the value in the control propagates
                $obj->get_submitted_value();

                // because we want this control to appear right beneath the select control when the form is auto-generated
                // we need to have it after the select control in the "controls" property

                // as is we just added the control, it means it is at the end of the array
                // we take it off the end of array
                $obj = array_pop($this->controls);

                // find the position of the parent control
                $parent_position = array_search($attributes['name'], array_keys($this->controls));

                // if PHP version is greater that 5.0.2
                if ($this->form_properties['php_version'] > 50200)

                    // use this method to insert the control right after the parent control
                    $this->controls =

                        array_slice($this->controls, 0, $parent_position + 1, true) +

                        array($attributes['id'] . $this->form_properties['other_suffix'] => $obj) +

                        array_slice($this->controls, $parent_position + 1, count($this->controls) - $parent_position, true);

                // for lower PHP versions
                else {

                    // this will hold the modified array
                    $tmp = array();

                    $counter = 0;

                    // iterate through the form's controls
                    foreach ($this->controls as $key => $control) {

                        // the controls that are before the parent control and the parent control
                        if ($counter <= $parent_position)

                            // add them to our temporary array
                            $tmp[$key] = $control;

                        // after we added the parent control
                        elseif ($counter == $parent_position + 1) {

                            // insert the new control
                            $tmp[$attributes['id'] . $this->form_properties['other_suffix']] = $obj;

                            // also insert the control that was previously at this position
                            $tmp[$key] = $control;

                        // for all controls after the parent control
                        } else

                            // add them to the temporary array
                            $tmp[$key] = $control;

                        ++$counter;

                    }

                    // make the controls array a copy of the temporary array
                    $this->controls = $tmp;

                }

            }

            // if control is a label and is a "master" label
            if (isset($attributes['type']) && $attributes['type'] == 'label' && array_key_exists($attributes['for'], $this->master_labels))

                // save the "master" label's name
                $this->master_labels[$attributes['for']]['control'] = $attributes['name'];

            // if control is a date control
            if (isset($attributes['type']) && $attributes['type'] == 'text' && preg_match('/\bdate\b/i', $attributes['class'])) {

                // if variable is not yet defined. define it
                if (!isset($datepicker_javascript)) $datepicker_javascript = '';

                // append the new date picker object
                $datepicker_javascript .= '$(\'#' . $attributes['id'] . '\').Zebra_DatePicker(';

                $control->attributes['days'] = $this->form_properties['language']['days'];

                $control->attributes['months'] = $this->form_properties['language']['months'];

                $properties = '';

                // iterate through control's attributes
                foreach ($control->attributes as $attribute => $value) {

                    // if attribute is an attribute intended for the javascript object and is not null
                    if (in_array($attribute, $control->javascript_attributes) && ($control->attributes[$attribute] !== null || ($attribute == 'direction' && $value === false))) {

                        // append to the properties list (we change "inside_icon" to "inside" as "inside" is reserved)
                        $properties .= ($properties != '' ? ',' : '') . ($attribute == 'inside_icon' ? 'inside' : $attribute) . ':';

                        // if value is an array
                        if (is_array($value)) {

                            // format accordingly
                            $properties .= '[';

                            foreach ($value as $val)

                                $properties .= ($val === true ? 'true' : ($val === false ? 'false' : '\'' . $val . '\'')) . ',';

                            $properties = rtrim($properties, ',') . ']';

                        // if value is a string but is not a javascript object
                        } elseif (is_string($value) && !preg_match('/^\{.*\}$/', trim($value)))

                            // format accordingly
                            $properties .= '\'' . $value . '\'';

                        // for any other case (javascript object, boolean)
                        else

                            // format accordingly
                            $properties .= ($value === true ? 'true' : ($value === false ? 'false' : $value));

                    }

                }

                // wrap up the javascript object
                $datepicker_javascript .= ($properties != '' ? '{' . $properties . '}' : '') . ');';

            }

        }

        // we add automatically this hidden control to the form , used to know that the form was submitted
        $this->add('hidden', $this->form_properties['identifier'], $this->form_properties['name']);

        // add a "honeypot" - a text field that we'll use to try and prevent spam-bots
        // this field will be hidden from users and we expect only spam-bots to fill it. if this field will not be empty
        // when submitting the form, we'll consider that the form was submitted by a spam-bot
        $this->add('text', $this->form_properties['honeypot'], '', array('autocomplete' => 'off'));

        // enable protection against CSRF attacks using the default values
        // note that this has no effect if this method was already called before
        $this->csrf();

        // if CSRF protection is enabled (is not boolean FALSE)
        if ($this->form_properties['csrf_storage_method'] !== false)

            // add a hidden field to the form, containing the random token
            // (we will later compare the value in this field with the value in the associated session/cookie)
            $this->add('hidden', $this->form_properties['csrf_token_name'], $this->form_properties['csrf_token']);

        // start rendering the form's hidden controls
        $output .= '<div class="hidden">';

        // iterate through the controls assigned to the form, looking for hidden controls
        // also, use this opportunity to see which labels are attached to "required" controls
        foreach ($this->controls as $key => $control) {

            // get some attributes for each control
            $attributes = $control->get_attributes(array('type', 'for', 'name', 'label', 'inside'));

            // sanitize the control's name
            $attributes['name'] = preg_replace('/\[\]$/', '', $attributes['name']);

            // if control is a "hidden" control
            if ($attributes['type'] == 'hidden') {

                // append the hidden control to the hidden control's block
                $output .= $control->toHTML();

                // take the control out of the controls array because we don't have to bother with it anymore
                unset($this->controls[$key]);

            // if control is a text field and is the control intended for the "honeypot"
            } elseif ($attributes['type'] == 'text' && $attributes['name'] == $this->form_properties['honeypot']) {

                // because http://www.w3.org/WAI/GL/WCAG20/WD-WCAG20-TECHS/html.html#H44 requires it,
                // attach a label to the control
                $output .= '<label for="' . $this->form_properties['honeypot'] .  '" style="display:none">Leave this field blank</label>';

                // append the control to the hidden control's block (it will not be visible)
                $output .= $control->toHTML();

                // take the control out of the controls array so we don't have to bother with it anymore
                unset($this->controls[$key]);

            // if
            } elseif (

                // control is a label AND
                $attributes['type'] == 'label' &&

                // has the "for" attribute set
                isset($attributes['for']) &&

                (

                    // represents a label for a group of checkboxes or radio buttons
                    array_key_exists($attributes['for'], $this->master_labels) ||

                    // the label is attached to an existing control
                    array_key_exists($attributes['for'], $this->controls)

                )

            ) {

                // if the label is attached to an existing control
                if (array_key_exists($attributes['for'], $this->controls)) {

                    // get some attributes of the control the label is attached to
                    $ctrl_attributes = $this->controls[$attributes['for']]->get_attributes(array('name', 'id', 'type'));

                    // sanitize the control's name
                    $ctrl_attributes['name'] = preg_replace('/\[\]$/', '', $ctrl_attributes['name']);

                    // if
                    if (

                        // the label has the "inside" attribute set
                        isset($attributes['inside']) &&

                        // the label's "inside" attribute is set to TRUE AND
                        $attributes['inside'] === true &&

                        // the type of the control the label is attached to is either text, textarea or password
                        (isset($ctrl_attributes['type']) && (

                            $ctrl_attributes['type'] == 'text' ||

                            $ctrl_attributes['type'] == 'textarea' ||

                            $ctrl_attributes['type'] == 'password'

                        ))

                    ) {

                        // set some extra attributes for the control the label is attached to
                        $this->controls[$attributes['for']]->set_attributes(array(

                            // for textareas we set the "title" attribute while for the text and password
                            // controls we set the "alt" attribute
                            'title' => $attributes['label'],

                        ));

                        // set some extra attributes for the control the label is attached to
                        $this->controls[$attributes['for']]->set_attributes(array(

                            // set a class, used by the JavaScript to set some extra attributes at runtime
                            'class' => 'inner-label',

                        ), false);

                    // if the control the label is attached to a radio button or a checkbox
                    } elseif ($ctrl_attributes['type'] == 'radio' || $ctrl_attributes['type'] == 'checkbox')

                        // set a specific class for the label control
                        $control->set_attributes(array('class' => 'option'));

                }

                // if the control the label is attached to has the "disabled" attribute set
                if (isset($this->controls[$attributes['for']]->attributes['disabled']))

                    // set a special class for the label
                    $control->set_attributes(array('class' => 'disabled'), false);

                // if the control the label is attached to, has the "required" rule set
                if (isset($this->controls[$attributes['for']]->rules['required']))

                    // if
                    if (

                        // a "master" label could exist for the control the label is attached to
                        array_key_exists($ctrl_attributes['name'], $this->master_labels) &&

                        // and a control that could be the "master" label exists
                        isset($this->controls[$this->master_labels[$ctrl_attributes['name']]['control']])

                    ) {

                        // if asterisk is not already attached
                        if (strpos($this->controls[$this->master_labels[$ctrl_attributes['name']]['control']]->attributes['label'], '<span class="required">*</span>') === false)

                            // attach the asterisk to the "master" label instead rather than to the current label
                            $this->controls[$this->master_labels[$ctrl_attributes['name']]['control']]->set_attributes(array('label' => $this->controls[$this->master_labels[$ctrl_attributes['name']]['control']]->attributes['label'] . '<span class="required">*</span>'));

                    // otherwise
                    } else

                        // attach the asterisk to the current label
                        $this->controls[$key]->set_attributes(array('label' => $attributes['label'] . '<span class="required">*</span>'));

            // if
            } elseif (

                // control is a label AND
                $attributes['type'] == 'label' &&

                // has the "for" attribute set
                isset($attributes['for']) &&

                // is neither a "master" label for a group of checkboxes or radio buttons
                !array_key_exists($attributes['for'], $this->master_labels) &&

                // nor is attached to an existing control
                !array_key_exists($attributes['for'], $this->controls) &&

                // we're not on autopilot (if we are, we will remove the "for" attribute later on)
                ($template != '' && $template != '*horizontal' && $template != '*vertical')

            // remove the "for" attribute so that the form will pass the W3C validation
            ) unset($this->controls[$key]->attributes['for']);

        }

        // finish building the hidden controls block
        $output .= '</div>';

        // if there are any error messages
        if (!empty($this->errors))

            // iterate through each error block
            foreach ($this->errors as $error_block => $error_messages) {

                $content = '';

                // iterate through each message of the error block
                foreach ($error_messages as $error_message) {

                    // render each message in block
                    $content .= '<span>' . $error_message . '</span>';

                    // if only one error message is to be show
                    // break out from the foreach loop
                    if ($this->form_properties['show_all_error_messages'] === false) break;

                }

                // switch the array entry with it's rendered form
                $this->errors[$error_block] = '<div class="error"><div class="container">' . $content . '<div class="close"><a href="javascript:void(0)">close</a></div></div></div>';

            }

        // if output is to be auto-generated
        if ($template == '' || $template == '*horizontal' || $template == '*vertical') {

            $error_messages = '';

            // iterate through any existing error blocks
            // and render them at the top of the auto-generated output
            foreach ($this->errors as $errors) $error_messages .= $errors;

            // group controls in master label/control/label/note
            $blocks = array();

            // iterate through the form's controls
            foreach ($this->controls as $key=>$control) {

                // get some attributes for the control
                $attributes = $control->get_attributes(array('type', 'name', 'id', 'for', 'inside'));

                // if control is a label that is to be placed inside another control, we skip it
                if (isset($attributes['inside'])) continue;

                // sanitize control's name
                $attributes['name'] = preg_replace('/\[\]$/', '', $attributes['name']);

                // if the control is a text box that is to be shown when user selects "other" in a select control
                if (preg_match('/(.*)' . preg_quote($this->form_properties['other_suffix']) . '$/', $attributes['name'], $matches) > 0)

                    // save the control the current control is attached to
                    $parent = $matches[1];

                // for other controls
                else {

                    // check the control's type
                    switch ($attributes['type']) {

                        // if control is captcha, label or note
                        case 'captcha':
                        case 'label':
                        case 'note':

                            // save the control the current control is attached to
                            $parent = $attributes['for'];

                            // if
                            if (

                                // parent control exist AND
                                isset($this->controls[$parent]) &&

                                // control is a checkbox or radio button
                                ($this->controls[$parent]->attributes['type'] == 'checkbox' || $this->controls[$parent]->attributes['type'] == 'radio') &&

                                // the parent control's ID is different the parent control's name
                                // (as is the case for radio buttons and checkboxes)
                                $this->controls[$parent]->attributes['id'] != $this->controls[$parent]->attributes['name']

                            )

                                // save the both the "master" parent and, separated by a dot, the actual parent
                                $parent = preg_replace('/\[\]$/', '', $this->controls[$parent]->attributes['name']) . '.' . $parent;

                            // if control is a label and the parent control doesn't exist (the label is most probably a "master" label)
                            elseif ($attributes['type'] == 'label' && !isset($this->controls[$parent]))

                                // remove the "for" attribute so that the form will pass the W3C validation
                                unset($this->controls[$key]->attributes['for']);

                            break;

                        // for any other controls
                        default:

                            // the parent is the control itself
                            $parent = $attributes['name'];

                    }

                }

                // as some controls (labels for checkboxes) can have multiple parent - the checkbox control and a master
                // label - and multiple parents are separated by a dot, explode by dot
                $parents = explode('.', $parent);

                // iterate through the control's parents
                foreach ($parents as $key => $parent) {

                    // if firs entry
                    // make $array a pointer to the $blocks array
                    if ($key == 0) $array = & $blocks;

                    // if the parent control doesn't have its own key in the array
                    // (it may still be in the array but not as a "parent")
                    if (!isset($array[$parent])) {

                        // initialize the entry
                        $array[$parent] = array();

                        // this works only on PHP 4.2.0 or above
                        // if we already have the entry but not as a key
                        if (($pos = array_search($parent, $array)) !== false) {

                            // insert it in the newly created entry
                            $array[$parent][] = $array[$pos];

                            // and remove it from the old position
                            unset($array[$pos]);

                        }

                    }

                    // make $array a pointer
                    $array = & $array[$parent];

                    // if we're at the last parent
                    if ($key == count($parents) - 1)

                        // if control already exits in the parent's array as a key (remember that $array is a pointer!)
                        if (array_key_exists($attributes['id'], $array))

                            // add the control to the array
                            $array[$attributes['id']][] = $attributes['id'];

                        // if control doesn't exit in the parent's array (remember that $array is a pointer!)
                        else

                            // add the control to the array
                            $array[] = $attributes['id'];

                }

            }

            // if auto-generated output needs to be horizontal
            if ($template == '*horizontal') {

                // the output will be enclosed in a table
                $contents = '<table>';

                // if there are errors to be displayed
                if ($error_messages != '')

                    // show the error messages
                    $contents .= '<tr><td colspan="2">' . $error_messages . '</td></tr>';

                // keep track of odd/even rows
                $counter = 0;

                // total number of rows to be displayed
                $rows = count($blocks);

                // iterate through blocks
                foreach ($blocks as $controls) {

                    ++$counter;

                    // each block is in its own row
                    $contents .= '<tr class="row' . ($counter % 2 == 0 ? ' even' : '') . ($counter == $rows ? ' last' : '') . '">';

                    // the first cell will hold the label (if any)
                    $contents .= '<td valign="top">';

                    // as of PHP 5.3, array_shift required the argument to be a variable and not the result
                    // of a function so we need this intermediary step
                    $labels = array_values($controls);

                    // retrieve the first item in the block
                    $label = array_shift($labels);

                    // item is a label
                    if (!is_array($label) && $this->controls[$label]->attributes['type'] == 'label') {

                        // remove it from the block
                        array_shift($controls);

                        // render it
                        $contents .= $this->controls[$label]->toHTML();

                    }

                    // close the table cell
                    $contents .= '</td>';

                    // the second cell contains the actual controls
                    $contents .= '<td valign="top">';

                    // iterate through the controls to be rendered
                    foreach ($controls as $control) {

                        // if array of controls
                        // (radio buttons/checkboxes and their labels)
                        if (is_array($control)) {

                            // iterate through the array's items
                            foreach ($control as $ctrl)

                                // and display them on the same line
                                $contents .= '<div class="cell">' . $this->controls[$ctrl]->toHTML() . '</div>';

                            // clear floats
                            //$contents .= '<div class="clear"></div>';  //checkbox

                        // if not an array of controls
                        } else

                            // if control is required but has the label as a tip inside the control
                            // we need to manually add the asterisk after the control
                            if (array_key_exists('required', $this->controls[$control]->rules) && preg_match('/\binner\-label\b/', $this->controls[$control]->attributes['class'])) {

                                // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                                $this->controls[$control]->set_attributes(array('class' => 'inline'), false);

                                // add the required symbol after the control
                                $contents .= $this->controls[$control]->toHTML() . '<span class="required">*</span>';

                            // else, render the control
                            } else $contents .= $this->controls[$control]->toHTML();

                    }

                    // close the cell
                    $contents .= '</td>';

                    // add a "separator" row
                    $contents .= '</tr>';

                }

                // finish rendering the table
                $contents .= '</table>';

            // if auto-generated output needs to be vertical
            } else {

                $contents = '';

                // if there are errors to be displayed, show the error messages
                if ($error_messages != '') $contents .= $error_messages;

                $counter = 0;

                // total number of rows to be displayed
                $rows = count($blocks);

                // iterate through blocks
                foreach ($blocks as $controls) {

                    // each block is in its own row
                    $contents .= '<div class="row' . (++$counter % 2 == 0 ? ' even' : '') . ($counter == $rows ? ' last' : '') . '">';

                    // iterate through the controls to be rendered
                    foreach ($controls as $control) {

                        // if array of controls
                        // (radio buttons/checkboxes and their labels)
                        if (is_array($control)) {

                            // iterate through the array's items
                            foreach ($control as $ctrl)

                                // and display them on the same line
                                $contents .= '<div class="cell">' . $this->controls[$ctrl]->toHTML() . '</div>';

                            // clear floats
                           //$contents .= '<div class="clear"></div>';

                        // if not an array of controls
                        } else

                            // if control is required but has the label as a tip inside the control
                            // we need to manually add the asterisk after the control
                            if (array_key_exists('required', $this->controls[$control]->rules) && preg_match('/\binner\-label\b/', $this->controls[$control]->attributes['class'])) {

                                // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                                $this->controls[$control]->set_attributes(array('class' => 'inline'), false);

                                // add the required symbol after the control
                                $contents .= $this->controls[$control]->toHTML() . '<span class="required">*</span>';

                            // else, render the control
                            } else $contents .= $this->controls[$control]->toHTML();

                    }

                    // finish rendering
                     $contents .= '</div>';
			
                }

            }

        // if a function with the name given as $template exists
        } elseif (is_array($template) || function_exists($template)) {

            // this variable will contain all the rendered controls
            $controls = array();

            // iterate through the controls assigned to the form
            foreach ($this->controls as $control) {

                // read some attributes of the control
                $attributes = $control->get_attributes(array('id', 'inside'));

                // render the control if the control is not a label that is to be displayed inside the control as it's
                // default value
                if (!isset($attributes['inside']))

                    // if control is required but has the label as a tip inside the control
                    // we need to manually add the asterisk after the control
                    if (array_key_exists('required', $control->rules) && preg_match('/\binner\-label\b/', $control->attributes['class'])) {

                        // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                        $control->set_attributes(array('class' => 'inline'), false);

                        // add the required symbol after the control
                        // and add generated HTML code to the $controls array
                        $controls[$attributes['id']] = $control->toHTML() . '<span class="required">*</span>';

                    // otherwise, add generated HTML code to the $controls array
                    } else $controls[$attributes['id']] = $control->toHTML();

            }

            // iterate through the variables assigned to the form
            foreach ($this->variables as $variable_name => $variable_value)

                // make available the assigned variables
                $controls[$variable_name] = $variable_value;

            // let the custom function generate the output
            // we're passing two arguments
            // an associative array with control ids and their respectively generated HTML
            // and an array with all the form's objects
            $contents = call_user_func_array($template, array($controls, &$this->controls));

        // if a template was specified
        } else {

            // this variable will contain all the rendered controls
            $controls = array();

            // iterate through the controls assigned to the form
            foreach ($this->controls as $control) {

                // read some attributes of the control
                $attributes = $control->get_attributes(array('id', 'inside'));

                // render the control if the control is not a label that is to be displayed inside the control as it's
                // default value
                if (!isset($attributes['inside']))

                    // if control is required but has the label as a tip inside the control
                    // we need to manually add the asterisk after the control
                    if (array_key_exists('required', $control->rules) && preg_match('/\binner\-label\b/', $control->attributes['class'])) {

                        // first, make sure the control is inline so that the asterisk will be placed to the right of the control
                        $control->set_attributes(array('class' => 'inline'), false);

                        // add the required symbol after the control
                        // and add generated HTML code to the $controls array
                        $controls[$attributes['id']] = $control->toHTML() . '<span class="required">*</span>';

                    // otherwise, add generated HTML code to the $controls array
                    } else $controls[$attributes['id']] = $control->toHTML();

            }

            //start output buffering
            ob_start();

            // make available in the template all the form's objects
            $controls[$this->form_properties['name']] = &$this->controls;

            // make the user-defined variables available in the template file as PHP variables
            extract($this->variables);

            // make the rendered controls available in the template file as PHP variables
            extract($controls);

            // make the error messages available in the template file as PHP variables
            extract($this->errors);

            // include the template file
            include $template;

            // put the parsed content in a variable
            $contents = ob_get_contents();

            // clean buffers
            ob_end_clean();

        }

        // finish building the output
        $output = $output . $contents . '</form>';

        // this will hold the properties to be set for the JavaScript object
        $javascript_object_properties = '';

        // if there are properties to be set for the JavaScript object
        if (is_array($this->form_properties['clientside_validation']))

            // iterate through the properties
            foreach ($this->form_properties['clientside_validation'] as $key => $value)

                // save property
                $javascript_object_properties .=
                    ($javascript_object_properties != '' ? ',' : '') . $key . ':' .
                    ($value === true ? 'true' : ($value === false ? 'false' : '\'' . $value . '\''));

        // if client side validation is enabled and any rules are defined
        if (isset($clientside_error_messages))

            // save property
            $javascript_object_properties .= ($javascript_object_properties != '' ? ',' : '') . 'error_messages:{' . $clientside_error_messages . '}';

        // function name for initializing client-side validation
        $function_name = 'init_' . md5(strtolower($this->form_properties['name'] . microtime()));
        $output .=
            '<script type="text/javascript">function ' . $function_name . '(){if(typeof jQuery=="undefined"||typeof jQuery.fn.Zebra_Form=="undefined"' .
            (isset($datepicker_javascript) ? '|| jQuery.fn.Zebra_DatePicker=="undefined"' : '') . '){setTimeout("' . $function_name . '()",100);return}' .
            (isset($datepicker_javascript) ? $datepicker_javascript : '') .
            '$("#' . $this->form_properties['name'] . '").Zebra_Form(' . ($javascript_object_properties != '' ? '{' . $javascript_object_properties . '}' : '') . ')}' .
            $function_name . '()</script>';

        // if $return argument was TRUE, return the result
        if ($return) return $output;

        // if $return argument was FALSE, output the content
        else echo $output;

    }

    /**
     *  Resets the submitted values for all of the form's controls (also resets the POST/GET/FILES superglobals)
     *
     *  @return void
     */
    function reset()
    {

        // iterate through the form's controls
        foreach ($this->controls as $key=>$control) {

            // reference to the control
            $obj = & $this->controls[$key];

            // reset
            $obj->reset();

        }

    }

    /**
     *  Sets how error messages generated upon server-side validation are displayed in an
     *  {@link Zebra_Form_Control::set_rule() error block}.
     *
     *  Client-side validation is done on the "onsubmit" event of the form. See {@link client_side_validation()} for
     *  more information on client-side validation.
     *
     *  <code>
     *  //  create a new form
     *  $form = new Zebra_Form('my_form');
     *
     *  //  display all error messages of error blocks
     *  $form->show_all_error_messages(true);
     *  </code>
     *
     *  @param  boolean $value  Setting this argument to TRUE will display <i>all</i> error messages of an error block,
     *                          while setting it to FALSE will display one error message at a time.
     *
     *                          Default is FALSE.
     *
     *
     *  @return                 void
     */
    function show_all_error_messages($value = false)
    {

        // set the property
        $this->form_properties['show_all_error_messages'] = $value;

    }

    /**
     *  This method performs the server-side validation of all the form's controls, making sure that all the values
     *  comply to the rules set for these controls through the {@link Zebra_Form_Control::set_rule() set_rule()} method.
     *
     *  Only by calling this method will the form's controls update their values. If this method is not called, all
     *  the controls will preserve their default values after submission even if these values were altered prior to
     *  submission.
     *
     *  This method must be called <b>before</b> the {@link render()} method or error messages will not be
     *  available.
     *
     *  After calling this method, if there are {@link Zebra_Form_File file} controls on the form, you might want to check
     *  for the existence of the {@link $file_upload} property to see the details of uploaded files and take actions
     *  accordingly.
     *
     *  Client-side validation is done on the "onsubmit" event of the form. See {@link client_side_validation()} for
     *  more information on client-side validation.
     *
     *  @return boolean     Returns TRUE if every rule was obeyed, FALSE if not.
     */
    function validate()
    {

        // reference to the form submission method
        global ${'_' . $this->form_properties['method']};

        $method = & ${'_' . $this->form_properties['method']};

        // enable protection against CSRF attacks using the default values
        // note that this has no effect if this method was already called before
        $this->csrf();

        // we assume the form is not valid (or it was not submitted)
        $form_is_valid = false;

        // continue only if form was submitted
        if (


            isset($method[$this->form_properties['identifier']]) &&

            $method[$this->form_properties['identifier']] == $this->form_properties['name']

        ) {

            // if
            if (

                // the "honeypot" field was submitted AND
                isset($method[$this->form_properties['honeypot']]) &&

                // the "honeypot" field is empty
                $method[$this->form_properties['honeypot']] == '' &&

                // no possible CSRF attacks detected
                ($csrf_status = $this->_csrf_validate())

            ) {

                // remove the honeypot and csrf entries so that we don't polute the $_POST array
                unset($method[$this->form_properties['honeypot']]);
                unset($method[$this->form_properties['csrf_token_name']]);

                // by default, we assume that the form is valid
                $form_is_valid = true;

                // iterate through the controls
                foreach (array_keys($this->controls) as $key) {

                    // reference to control
                    $control = & $this->controls[$key];

                    // get some attributes of the control
                    $attribute = $control->get_attributes(array('type'));

                    // validate the control
                    $valid = $this->validate_control($key);

                    // do some extra checkings and cleanup
                    if (

                        //if type is password OR
                        $attribute['type'] == 'password' ||

                        //if type is text and has the "captcha" rule set
                        ($attribute['type'] == 'text' && isset($control->rules['captcha']))

                    // clear the value in the field
                    ) $control->set_attributes(array('value' => ''));

                    // if control is not valid, the form is not valid
                    if (!$valid) $form_is_valid = false;

                }

                // after iterating through all the controls,
                // check if the form is still valid
                if ($form_is_valid)

                    // if there are any actions to be performed when the form is valid
                    // (file upload, resize, convert)
                    if (isset($this->actions) && !empty($this->actions))

                        // iterate through the actions
                        foreach ($this->actions as $actions)

                            // if the respective action (method) exists
                            if (method_exists($this, $actions[0])) {

                                // if the method was erroneous
                                if (!call_user_func_array(array(&$this,$actions[0]), array_slice($actions, 1))) {

                                    // add error message to indicated error block
                                    $this->add_error($actions['block'], $actions['message']);

                                    // set the form as not being valid
                                    $form_is_valid = false;

                                }

                            // if the task (method) could not be found, trigger an error message
                            } else _zebra_form_show_error('Method ' . $actions[0] . ' does not exist!', E_USER_ERROR);

            // else if
            } elseif (

                // honeypot field was not submitted
                !isset($method[$this->form_properties['honeypot']]) ||

                // honeypot field is not empty
                $method[$this->form_properties['honeypot']] != ''

            // show the appropriate error message to the user
            ) $this->add_error('*spam*', $this->form_properties['language']['spam_detected']);

            // else, if a possible CSRF attack was detected
            // show the appropriate error message to the user
            elseif (!$csrf_status) $this->add_error('*spam*', $this->form_properties['language']['csrf_detected']);

        // here's a special error check:
        // due to a bug (?) when the POST/GET data is larger than allowed by upload_max_filesize/post_max_size the
        // $_POST/$_GET/$_FILES superglobals are empty (see http://bugs.php.net/bug.php?id=49570)
        // but still, we need to present the user with some error message...
        } elseif (empty($method) && isset($_SERVER['CONTENT_LENGTH']) && (int)$_SERVER['CONTENT_LENGTH'] > 0)

            $form_is_valid = false;

        // if 
        if (

            // form is valid
            $form_is_valid ||

            // form is invalid and the from was not submitted via AJAX
            !isset($_SERVER['HTTP_X_REQUESTED_WITH'])

        // regenerate the CSRF token
        ) $this->_csrf_generate_token(true);

        // return the state of the form
        return $form_is_valid;

    }

    /**
     *  This method performs the server-side validation of a control, making sure that the value complies to the rules
     *  set for the control through the {@link Zebra_Form_Control::set_rule() set_rule()} method.
     *
     *  @param  string  $control    Unique name that identifies the control in the form.
     *
     *  @return boolean             Returns TRUE if every rule was obeyed, FALSE if not.
     */
    function validate_control($control)
    {

        // reference to the form submission method
        global ${'_' . $this->form_properties['method']};

        $method = & ${'_' . $this->form_properties['method']};

        // at this point, we assume that the control is not valid
        $valid = false;

        // continue only if form was submitted
        if (

            isset($method[$this->form_properties['identifier']]) &&

            $method[$this->form_properties['identifier']] == $this->form_properties['name']

        ) {

            // at this point, we assume that the control is valid
            $valid = true;

            // reference to control
            $control = & $this->controls[$control];

            // manage submitted value
            $control->get_submitted_value();

            // get some attributes of the control
            $attribute = $control->get_attributes(array('name', 'type', 'value', 'multiple', 'format', 'disable_spam_filter', 'other'));

            // if control doesn't have the SPAM filter disabled
            if (!isset($attribute['disable_spam_filter']) || $attribute['disable_spam_filter'] !== true) {

                // check to see if there is SPAM/INJECTION attempt by checking if the values in select boxes, radio buttons
                // and checkboxes are in the list of allowable values, as set when initializing the controls

                // check controls by type
                switch ($attribute['type']) {

                    // if control is a select box
                    case 'select':

                        // if control was submitted
                        // (as there can also be no selections for a select box with the "multiple" attribute set, case in
                        // which there's no submission)
                        if ($control->submitted_value) {

                            // flatten array (in case we have select groups)
                            $values = $this->_extract_values($control->attributes['options']);

                            // if the "other" attribute is set, then "other" is a valid option
                            if (isset($attribute['other'])) $values[] = 'other';

                            // we need to treat all values as strings
                            // or the in_array below will fail in strict mode
                            array_walk($values, create_function('&$value', '$value = (string)$value;'));

                            // if an array was submitted and there are values that are not in the list allowable values
                            if (is_array($control->submitted_value) && $control->submitted_value != array_intersect($control->submitted_value, $values))

                                // set a flag accordingly
                                $valid = false;

                            // if submitted value is not an array and submitted value is not in the list of allowable values
                            // we use strict mode or any string, when compared to 0, will be valid...
                            if (!is_array($control->submitted_value) && !in_array($control->submitted_value, $values, true))

                                // set a flag accordingly
                                $valid = false;

                        }

                        break;

                    // if control is a checkbox control or a radio button
                    case 'checkbox':
                    case 'radio':

                        // if control was submitted
                        if ($control->submitted_value) {

                            $values = array();

                            // iterate through all the form's controls
                            foreach ($this->controls as $element)

                                // if control is of the same type and has the same name
                                if ($element->attributes['type'] == $attribute['type'] && $element->attributes['name'] == $attribute['name'])

                                    // add the control's value to the list of valid values
                                    $values[] = $element->attributes['value'];

                            // if an array was submitted and there are values that are not in the list allowable values
                            if (is_array($control->submitted_value) && $control->submitted_value != array_intersect($control->submitted_value, $values))

                                // set a flag accordingly
                                $valid = false;

                            // if submitted value is not an array and submitted value is not in the list of allowable values
                            if (!is_array($control->submitted_value) && !in_array($control->submitted_value, $values))

                                // set a flag accordingly
                                $valid = false;

                        }

                        break;

                }

                // if spam attempt was detected
                if (!$valid) {

                    // set the error message
                    $this->add_error('*spam*', $this->form_properties['language']['spam_detected']);

                    // don't look further
                    return false;

                }

            }

            // if
            if (

                // control was submitted and has rules assigned
                isset($control->submitted_value) && !empty($control->rules)

            ) {

                // iterate through rules assigned to the control
                foreach ($control->rules as $rule_name => $rule_attributes) {

                    // make sure the rule name is in lowercase
                    $rule_name = strtolower($rule_name);

                    // check the rule's name
                    switch ($rule_name) {

                        // if rule is 'alphabet'
                        case 'alphabet':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // control does not contain only letters from the alphabet (and other allowed characters, if any)
                                !preg_match('/^[a-z' . preg_quote($rule_attributes[0]) . ']+$/i', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if rule is 'alphanumeric'
                        case 'alphanumeric':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // control does not contain only allowed characters
                                !preg_match('/^[a-z0-9' . preg_quote($rule_attributes[0]) . ']+$/i', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if 'captcha'
                        case 'captcha':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // control's value is not the one showed in the picture
                                md5(md5(md5(strtolower($control->submitted_value)))) !=  @$_COOKIE['captcha']

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if 'compare'
                        case 'compare':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) && (

                                    // the control to compare to was not submitted
                                    !isset($method[$rule_attributes[0]]) ||

                                    // OR
                                    (

                                        // the control to compare to was submitted
                                        isset($method[$rule_attributes[0]]) &&

                                        // and the values don't match
                                        $control->submitted_value != $method[$rule_attributes[0]]

                                    )

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if 'convert'
                        case 'convert':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without any errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // as conversions are done only when the form is valid
                                // for now we only save some data that will be processed if the form is valid
                                // (we're adding keys so that we don't have duplicate actions if validate_control method is called repeatedly)
                                $this->actions[$attribute['name'] . '_convert'] = array(

                                    '_convert',                                             //  method to be called
                                    $attribute['name'],                                     //  the file upload control's name
                                    'extension'                 =>  $rule_attributes[0],    //  extension to convert to
                                    'quality'                   =>  $rule_attributes[1],    //  quality (available only for JPEG files)
                                    'preserve_original_file'    =>  $rule_attributes[2],    //  preserve original file?
                                    'overwrite'                 =>  $rule_attributes[3],    //  overwrite if file with new extension exists
                                    'block'                     =>  $rule_attributes[4],    //  error block
                                    'message'                   =>  $rule_attributes[5],    //  error message

                                );

                            }

                            break;

                        // if 'custom' rule
                        case 'custom':

                            // custom rules are stored as an array
                            // iterate through the custom rules
                            foreach ($rule_attributes as $custom_rule_attributes) {

                                // if custom function exists
                                if (function_exists($custom_rule_attributes[0])) {

                                    // the arguments that we are passing to the custom function are the control's
                                    // submitted value and all other arguments passed when setting the custom rule
                                    // except the first one which is the custom function's and the last two which are
                                    // the error block name and the error message respectively
                                    $arguments = array_merge(array($control->submitted_value), array_slice($custom_rule_attributes, 1, -2));

                                    // run the custom function
                                    // and if the function returns false
                                    if (!call_user_func_array($custom_rule_attributes[0], $arguments)) {

                                        // count the arguments passed when declaring the rules
                                        $attributes_count = count($custom_rule_attributes);

                                        // add error message to indicated error block
                                        $this->add_error($custom_rule_attributes[$attributes_count - 2], $custom_rule_attributes[$attributes_count - 1]);

                                        // the control does not validate
                                        $valid = false;

                                        // no further checking needs to be done for the control, making sure that only one
                                        // error message is displayed at a time for each erroneous control
                                        break 3;

                                    }

                                // if custom function doesn't exist, trigger an error message
                                } else _zebra_form_show_error('Function <strong>' . $custom_rule_attributes[0] . '()</strong> doesn\'t exist.', E_USER_ERROR);

                            }

                            break;

                        // if date
                        case 'date':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // is a 'date' control
                                isset($attribute['format']) &&

                                // a value was entered
                                $attribute['value'] != ''

                            ) {

                                // the format we expect the date to be in (white spaces removed)
                                // we are removing spaces so that a format like, for example, "M d Y, H:i" (note the space after the comma)
                                // will validate also if hour and minute come right after the comma
                                // also, escape characters that would make sense as regular expression
                                $format = preg_replace('/\s/', '', preg_quote($control->attributes['format']));

                                // parse the format and extract the characters that define the format
                                // (note that we're also capturing the offsets)
                                preg_match_all('/[dDjlNSwFmMnYyGHghaAisU]{1}/', $format, $matches, PREG_OFFSET_CAPTURE);

                                $regexp = array();

                                // iterate through the found characters
                                // and create the regular expression that we will use to see if the entered date is ok
                                foreach ($matches[0] as $match) {

                                    switch ($match[0]) {

                                        // day of the month, 2 digits with leading zeros, 01 to 31
                                        case 'd': $regexp[] = '0[1-9]|[12][0-9]|3[01]'; break;

                                        // a textual representation of a day, three letters, mon through sun
                                        case 'D': $regexp[] = '[a-z]{3}'; break;

                                        // day of the month without leading zeros, 1 to 31
                                        case 'j': $regexp[] = '[1-9]|[12][0-9]|3[01]'; break;

                                        // a full textual representation of the day of the week, sunday through saturday
                                        case 'l': $regexp[] = '[a-z]+'; break;

                                        // ISO-8601 numeric representation of the day of the week (added in PHP 5.1.0), 1 (for Monday) through 7 (for Sunday)
                                        case 'N': $regexp[] = '[1-7]'; break;

                                        // english ordinal suffix for the day of the month, 2 characters: st, nd, rd or th. works well with j
                                        case 'S': $regexp[] = 'st|nd|rd|th'; break;

                                        // numeric representation of the day of the week, 0 (for sunday) through 6 (for saturday)
                                        case 'w': $regexp[] = '[0-6]'; break;

                                        // a full textual representation of a month, such as january or march
                                        case 'F': $regexp[] = '[a-z]+'; break;

                                        // numeric representation of a month, with leading zeros, 01 through 12
                                        case 'm': $regexp[] = '0[1-9]|1[012]+'; break;

                                        // a short textual representation of a month, three letters, jan through dec
                                        case 'M': $regexp[] = '[a-z]{3}'; break;

                                        // numeric representation of a month, without leading zeros, 1 through 12
                                        case 'n': $regexp[] = '[1-9]|1[012]'; break;

                                        // a full numeric representation of a year, 4 digits examples: 1999 or 2003
                                        case 'Y': $regexp[] = '[0-9]{4}'; break;

                                        // a two digit representation of a year examples: 99 or 03
                                        case 'y': $regexp[] = '[0-9]{2}'; break;

                                        // 24-hour format of an hour without leading zeros, 0 through 23
                        				case 'G':

                                        // 24-hour format of an hour with leading zeros, 00 through 23
                        				case 'H':

                                        // 12-hour format of an hour without leading zeros, 1 through 12
                        				case 'g':

                                        // 12-hour format of an hour with leading zeros, 01 through 12
                        				case 'h': $regexp[] = '[0-9]{1,2}'; break;

                                        // lowercase ante meridiem and post meridiem am or pm
                        				case 'a':
                        				case 'A': $regexp[] = '(am|pm)'; break;

                                        // minutes with leading zeros, 00 to 59
                        				case 'i':

                                        // seconds, with leading zeros 00 through 59
                        				case 's': $regexp[] = '[012345][0-9]'; break;

                                    }

                                }

                                // if format is defined
                                if (!empty($regexp)) {

                                    // we will replace every format-related character in the format expression with
                                    // the appropriate regular expression in order to see that valid data was entered
                                    // as required by the character
                                    // we are replacing from finish to start so that we don't mess up the offsets
                                    // therefore, we need to reverse the array first
                                    $matches[0] = array_reverse($matches[0]);

                                    // how many characters to replace
                                    $chars = count($matches[0]);

                                    // iterate through the characters
                                    foreach ($matches[0] as $index => $char)

                                        // and replace them with the appropriate regular expression
                                        $format = substr_replace($format, '(' . $regexp[$chars - $index - 1] . ')', $matches[0][$index][1], 1);

                                    // the final regular expression to math the date against
                                    $format = '/^' . str_replace('/', '\/', $format) . '$/i';

                                    // if entered value (with spaces removed) seems to be ok
                                    if (preg_match($format, preg_replace('/\s/', '', $attribute['value']), $segments)) {

                                        $original_day = $original_month = $original_year = 0;

                                        // english names for days and months
                                        $english_days   = array('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday');
                                        $english_months = array('January','February','March','April','May','June','July','August','September','October','November','December');

                                        // reverse the characters in the format (remember that we reversed them above)
                                        $matches[0] = array_reverse($matches[0]);

                                        $valid = true;

                                        // iterate through the characters in the format
                                        // to see if months and days are correct
                                        // i.e. if for month we entered "abc" it would pass our regular expression but
                                        // now we will check if the three letter text is an actual month
                                        foreach ($matches[0] as $index => $match) {

                                            switch ($match[0]) {

                                                // numeric representation of a month, with leading zeros, 01 through 12
                                                case 'm':
                                                // numeric representation of a month, without leading zeros, 1 through 12
                                                case 'n':

                                                    $original_month = (int)($segments[$index + 1] - 1);

                                                    break;

                                                // day of the month, 2 digits with leading zeros, 01 to 31
                                                case 'd':
                                                // day of the month without leading zeros, 1 to 31
                                                case 'j':

                                                    $original_day = (int)($segments[$index + 1]);

                                                    break;

                                                // a textual representation of a day, three letters, mon through sun
                                                case 'D':
                                                // a full textual representation of the day of the week, sunday through saturday
                                                case 'l':
                                                // a full textual representation of a month, such as january or march
                                                case 'F':
                                                // a short textual representation of a month, three letters, jan through dec
                                                case 'M':

                                                    // by default, we assume that the text is invalid
                                                    $valid = false;

                                                    // iterate through the values in the language file
                                                    foreach ($this->language[($match[0] == 'F' || $match[0] == 'M' ? 'months' : 'days')] as $key => $value) {

                                                        // if value matches the value from the language file
                                                        if (strtolower($segments[$index + 1]) == strtolower(substr($value, 0, ($match[0] == 'D' || $match[0] == 'M' ? 3 : strlen($value))))) {

                                                            // replace with the english value
                                                            // this is because later on we'll run strtotime of the entered value and strtotime parses english dates
                                                            switch ($match[0]) {
                                                                case 'D': $segments[$index + 1] = substr($english_days[$key], 0, 3); break;
                                                                case 'l': $segments[$index + 1] = $english_days[$key]; break;
                                                                case 'F': $segments[$index + 1] = $english_months[$key]; $original_month = $key; break;
                                                                case 'M': $segments[$index + 1] = substr($english_months[$key], 0, 3); $original_month = $key; break;
                                                            }

                                                            // flag the value as valid
                                                            $valid = true;

                                                            // don't look further
                                                            break;

                                                        }

                                                    }

                                                    // if an invalid was found don't look any further
                                                    if (!$valid) break 2;

                                                    break;

                                                // a full numeric representation of a year, 4 digits examples: 1999 or 2003
                                                case 'Y':

                                                    $original_year = (int)($segments[$index + 1]);

                                                    break;

                                                // a two digit representation of a year examples: 99 or 03
                                                case 'y':

                                                    $original_year = (int)('19' . $segments[$index + 1]);

                                                    break;

                                            }

                                        }

                                        // if entered value seems valid
                                        if ($valid) {

                                            // if date is still valid after we process it with strtotime
                                            // (we do this because, so far, a date like "Feb 31 2010" would be valid
                                            // but strtotime would turn that to "Mar 03 2010")
                                            if (

                                                $english_months[$original_month] . ' ' . str_pad($original_day, 2, '0', STR_PAD_LEFT) . ', ' . $original_year ==
                                                date('F d, Y', strtotime($english_months[$original_month] . ' ' . $original_day . ', ' . $original_year))

                                            ) {

                                                // make sure we also return the date as YYYY-MM-DD so that it can be
                                                // easily used with a database or with PHP's strtotime function
                                                $control->attributes['date'] = $original_year . '-' . str_pad($original_month + 1, 2, '0', STR_PAD_LEFT) . '-' . str_pad($original_day, 2, '0', STR_PAD_LEFT);

                                                // control is valid
                                                break;

                                            }

                                        }

                                    }

                                }

                                // if scripts gets this far, it means there was an error somewhere

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "datecompare"
                        case 'datecompare':

                            if (

                                // control is 'text'
                                $attribute['type'] == 'text' &&

                                // is a 'date' control
                                isset($attribute['format']) &&

                                // control to compare with, exists
                                isset($this->controls[$rule_attributes[0]]) &&

                                // control to compare with, is a 'text' control
                                $this->controls[$rule_attributes[0]]->attributes['type'] == 'text' &&

                                // control to compare with, is a 'date' control
                                ($this->controls[$rule_attributes[0]]->attributes['format']) &&

                                // control validates
                                $this->validate_control($this->controls[$rule_attributes[0]]->attributes['id'])

                            ) {

                                // we assume the control is invalid
                                $valid = false;

                                // compare the controls according to the comparison operator
                                switch ($rule_attributes[1]) {
                                    case '>':
                                        $valid = ($control->attributes['date'] > $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                    case '>=':
                                        $valid = ($control->attributes['date'] >= $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                    case '<':
                                        $valid = ($control->attributes['date'] < $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                    case '<=':
                                        $valid = ($control->attributes['date'] <= $this->controls[$rule_attributes[0]]->attributes['date']);
                                        break;
                                }

                                // if invalid
                                if (!$valid) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[2], $rule_attributes[3]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if rule is 'digits'
                        case 'digits':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // but entered value does not contain digits only (and other allowed characters, if any)
                                !preg_match('/^[0-9' . preg_quote($rule_attributes[0]) . ']+$/', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "email"
                        case 'email':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // but is not a valid email address
                                !preg_match('/^([a-zA-Z0-9_\-\+\~\^\{\}]+[\.]?)+@{1}([a-zA-Z0-9_\-\+\~\^\{\}]+[\.]?)+\.[A-Za-z0-9]{2,}$/', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "list of emails"
                        case 'emails':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != ''

                            ) {

                                // convert string to an array of addresses
                                $addresses = explode(',', $attribute['value']);

                                // iterate through the addresses
                                foreach ($addresses as $address)

                                    // not a valid email address
                                    if (!preg_match('/^([a-zA-Z0-9_\-\+\~\^\{\}]+[\.]?)+@{1}([a-zA-Z0-9_\-\+\~\^\{\}]+[\.]?)+\.[A-Za-z0-9]{2,}$/', trim($address))) {

                                        // add error message to indicated error block
                                        $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                        // the control does not validate
                                        $valid = false;

                                        // no further checking needs to be done for the control, making sure that only one
                                        // error message is displayed at a time for each erroneous control
                                        break 3;

                                    }

                            }

                            break;

                        // if "filesize"
                        case 'filesize':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                (

                                    // uploaded file size exceeds the size imposed when creating the form
                                    $_FILES[$attribute['name']]['size'] > $rule_attributes[0] ||

                                    // the uploaded file exceeds the upload_max_filesize directive in php.ini
                                    $_FILES[$attribute['name']]['error'] == 1 ||

                                    // the uploaded file exceeds the MAX_FILE_SIZE directive that was specified
                                    // in the HTML form
                                    $_FILES[$attribute['name']]['error'] == 2

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "filetype"
                        case 'filetype':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // if file with mime types was not already loaded
                                if (!isset($this->mimes)) {

                                    // read file into an array
                                    $rows = file($this->form_properties['assets_server_path'] . 'mimes.json');

                                    // convert JSON to array
                                    // i'm aware that in PHP 5.2+ there is json_decode, but i want this library to be
                                    // as backward compatible as possible so, since the values in mimes.json has a
                                    // specific structure, i wrote my own decoder
                                    $this->mimes = array();

                                    // iterate through all the rows
                                    foreach ($rows as $row) {

                                        // if valid row found
                                        if (strpos($row, ':') !== false) {

                                            // explode the string by :
                                            $items = explode(':', $row);

                                            // the file type (extension)
                                            $index = trim(str_replace('"', '', $items[0]));

                                            // if there are more mime types attached
                                            if (strpos($items[1], '[') !== false)

                                                // convert to array
                                                $value = array_diff(array_map(create_function('&$value', 'return trim($value);'), explode(',', str_replace(array('[', ']', '"', '\/'), array('', '', '', '/'), $items[1]))), array(''));

                                            // if a single mime type is attached
                                            else

                                                // convert to string
                                                $value = trim(str_replace(array('"', ',', '\/'), array('', '', '/'), $items[1]));

                                            // save entry
                                            $this->mimes[$index] = $value;

                                        }

                                    }

                                }

                                // if "finfo_open" function exists (from PHP 5.3.0)
                                if (function_exists('finfo_open')) {

                                    // determine the "true" mime type of the uploaded file
                                    $finfo = finfo_open(FILEINFO_MIME_TYPE);
                                    $mime = finfo_file($finfo, $_FILES[$attribute['name']]['tmp_name']);
                                    finfo_close($finfo);

                                // otherwise, rely on the information returned by $_FILES which uses the file's
                                // extension to determine the uploaded file's mime type and is therefore unreliable
                                } else $mime = $_FILES[$attribute['name']]['type'];

                                // get the allowed file types
                                $allowed_file_types = array_map(create_function('$value', 'return trim($value);'), explode(',', $rule_attributes[0]));

                                // this will contain an array of file types that match for the currently uploaded file's
                                // mime type
                                $matching_file_types = array();

                                // iterate through the known mime types
                                foreach ($this->mimes as $extension => $type)

                                    // if
                                    if (

                                        // there are more mime types associated with the file extension and
                                        // the uploaded file's type is among them
                                        is_array($type) && in_array($mime, $type) ||

                                        // a single mime type is associated with the file extension and
                                        // the uploaded file's type matches the mime type
                                        !is_array($type) && $type == $mime

                                    // add file type to the list of file types that match for the currently uploaded
                                    // file's mime type
                                    ) $matching_file_types[] = $extension;

                                // is the file allowed?
                                $matches = array_intersect($matching_file_types, $allowed_file_types);

                                // if file is not allowed
                                if (empty($matches)) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if rule is 'float'
                        case 'float':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                (

                                    // only a dot given
                                    trim($attribute['value']) == '.' ||

                                    // only minus given
                                    trim($attribute['value']) == '-' ||

                                    // has too many minus sign
                                    preg_match_all('/\-/', $attribute['value'], $matches) > 1 ||

                                    // has too many dots in it
                                    preg_match_all('/\./', $attribute['value'], $matches) > 1 ||

                                    // not a floating point number
                                    !preg_match('/^[0-9\-\.' . preg_quote($rule_attributes[0]) . ']+$/', $attribute['value']) ||

                                    // has a minus sign in it but is not at the very beginning
                                    (strpos($attribute['value'], '-') !== false && strpos($attribute['value'], '-') > 0)

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "image"
                        case 'image':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // get some information about the file
                                list($width, $height, $type, $attr) = @getimagesize($_FILES[$attribute['name']]['tmp_name']);

                                // if file is not an image or image is not gif, png or jpeg
                                if ($type === false || $type < 1 || $type > 3) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if "length"
                        case 'length':

                            // the rule will be considered as not obeyed when
                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                (
                                    // the length of the value exceeds boundaries
                                    strlen($attribute['value']) < $rule_attributes[0] ||

                                    // we use the utf8_decode because some characters have 2 bytes and some 3 bytes
                                    // read more at http://globalizer.wordpress.com/2007/01/16/utf-8-and-string-length-limitations/
                                    ($rule_attributes[1] > 0 && strlen(utf8_decode($attribute['value'])) > $rule_attributes[1])

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[2], $rule_attributes[3]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if rule is 'number'
                        case 'number':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                (

                                    // only minus given
                                    trim($attribute['value']) == '-' ||

                                    // has too many minus sign
                                    preg_match_all('/\-/', $attribute['value'], $matches) > 1 ||

                                    // not a number
                                    !preg_match('/^[0-9\-' . preg_quote($rule_attributes[0]) . ']+$/', $attribute['value']) ||

                                    // has a minus sign in it but is not at the very beginning
                                    (strpos($attribute['value'], '-') !== false && strpos($attribute['value'], '-') > 0)

                                )

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "regexp"
                        case 'regexp':

                            if (

                                (
                                    // control is 'password'
                                    $attribute['type'] == 'password' ||

                                    // control is 'text'
                                    $attribute['type'] == 'text' ||

                                    // control is 'textarea'
                                    $attribute['type'] == 'textarea'

                                ) &&

                                // a value was entered
                                $attribute['value'] != '' &&

                                // value does not match regular expression
                                !preg_match('/' . $rule_attributes[0] . '/', $attribute['value'])

                            ) {

                                // add error message to indicated error block
                                $this->add_error($rule_attributes[1], $rule_attributes[2]);

                                // the control does not validate
                                $valid = false;

                                // no further checking needs to be done for the control, making sure that only one
                                // error message is displayed at a time for each erroneous control
                                break 2;

                            }

                            break;

                        // if "required"
                        case 'required':

                            // if it's a drop-down that is part of a time control
                            if ($attribute['type'] == 'time') {

                                // if invalid format specified, revert to the default "hm"
                                if (preg_match('/^[hmsg]+$/i', $attribute['format']) == 0 || strlen(preg_replace('/([a-z]{2,})/i', '$1', $attribute['format'])) != strlen($attribute['format'])) $attribute['format'] = 'hm';

                                $regexp = '';

                                // build the regular expression for validating the time
                                for ($i = 0; $i < strlen($attribute['format']); $i++) {

                                    // for each characher in the format we use a particular regular expression
                                    switch (strtolower(substr($attribute['format'], $i, 1))) {

                                        case 'h':

                                            // if 12 hour format is used use this expression...
                                            if (strpos(strtolower($attribute['format']), 'g')) $regexp .= '0[1-9]|1[012]';

                                            // ...and different expression for the 24 hour format
                                            else $regexp .= '([0-1][0-9]|2[0-3])';

                                            break;

                                        case 'm':
                                        case 's':

                                            // regular expression for validating minutes and seconds
                                            $regexp .= '[0-5][0-9]';

                                            break;

                                        case 'g':

                                            // validate am/pm
                                            $regexp .= '(am|pm)';

                                            break;

                                    }

                                }

                                // if time does not validate
                                if (preg_match('/' . $regexp . '/i', str_replace(array(':', ' '), '', $attribute['value'])) == 0) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            // for other controls
                            } else {

                                // if control is 'select'
                                if ($attribute['type'] == 'select') {

                                    // as of PHP 5.3, array_shift required the argument to be a variable and not the result
                                    // of a function so we need this intermediary step
                                    $notSelectedIndex = array_keys($control->attributes['options']);

                                    // get the index which when selected indicated that 'nothing is selected'
                                    $notSelectedIndex = array_shift($notSelectedIndex);

                                }

                                // the rule will be considered as not obeyed when
                                if (

                                    // control is 'password' or 'text' or 'textarea' and the 'value' attribute is empty
                                    (($attribute['type'] == 'password' || $attribute['type'] == 'text' || $attribute['type'] == 'textarea') && trim($attribute['value']) == '') ||

                                    // control is 'file' and no file specified
                                    ($attribute['type'] == 'file' && isset($_FILES[$attribute['name']]) && trim($_FILES[$attribute['name']]['name']) == '') ||

                                    // control is 'checkbox' or 'radio' and the control was not submitted
                                    (($attribute['type'] == 'checkbox' || $attribute['type'] == 'radio') && $control->submitted_value === false) ||

                                    // control is 'select', the 'multiple' attribute is set and control was not submitted
                                    ($attribute['type'] == 'select' && isset($attribute['multiple']) && $control->submitted_value === false) ||

                                    // control is 'select', the 'multiple' attribute is not set and the select control's first value is selected
                                    ($attribute['type'] == 'select' && !isset($attribute['multiple']) && (is_array($control->submitted_value) || strcmp($control->submitted_value, $notSelectedIndex) == 0)) ||

                                    // control is 'select', the 'multiple' attribute is not set, the select control's value is "other" and the "other" control is empty
                                    ($attribute['type'] == 'select' && !isset($attribute['multiple']) && $control->submitted_value == 'other' && trim($method[$attribute['name'] . $this->form_properties['other_suffix']]) == '')

                                ) {

                                    // add error message to indicated error block
                                    $this->add_error($rule_attributes[0], $rule_attributes[1]);

                                    // the control does not validate
                                    $valid = false;

                                    // no further checking needs to be done for the control, making sure that only one
                                    // error message is displayed at a time for each erroneous control
                                    break 2;

                                }

                            }

                            break;

                        // if 'resize'
                        case 'resize':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without any errors
                                $_FILES[$attribute['name']]['error'] == 0

                            ) {

                                // as of PHP 5.3, array_shift required the argument to be a variable and not the result
                                // of a function so we need this intermediary step
                                $tmp = array_values($rule_attributes);

                                // if not multiple resize calls
                                // make it look like multiple resize call
                                if (!is_array(array_shift($tmp)))

                                    $rule_attributes = array($rule_attributes);

                                // iterate through the resize calls
                                foreach ($rule_attributes as $index => $rule_attribute)

                                    // as resizes are done only when the form is valid and after the file has been
                                    // uploaded, for now we only save some data that will be processed if the form is valid
                                    // (we're adding keys so that we don't have duplicate actions if validate_control method is called repeatedly)
                                    $this->actions[$attribute['name'] . '_resize_' . $index] = array(

                                        '_resize',          //  method that needs to be called
                                        $attribute['name'], //  the file upload control's name
                                        $rule_attribute[0], //  prefix for the resized file
                                        $rule_attribute[1], //  width
                                        $rule_attribute[2], //  height
                                        $rule_attribute[3], //  preserve aspect ratio?
                                        $rule_attribute[4], //  method,
                                        $rule_attribute[5], //  background color
                                        $rule_attribute[6], //  enlarge smaller images?
                                        $rule_attribute[7], //  jpeg quality
                                        'block'     =>  $rule_attribute[8],  //  error block
                                        'message'   =>  $rule_attribute[9],  //  error message

                                    );

                            }

                            break;

                        // if 'upload'
                        case 'upload':

                            if (

                                // control is 'file'
                                $attribute['type'] == 'file' &&

                                // and a file was uploaded
                                isset($_FILES[$attribute['name']]) &&

                                // and file was uploaded without any errors
                                $_FILES[$attribute['name']]['error'] == 0

                            )

                                // as uploads are done only when the form is valid
                                // for now we only save some data that will be processed if the form is valid
                                // (we're adding keys so that we don't have duplicate actions if validate_control method is called repeatedly)
                                $this->actions[$attribute['name'] . '_upload'] = array(

                                    '_upload',                              //  method to be called
                                    $attribute['name'],                     //  the file upload control's name
                                    $rule_attributes[0],                    //  the folder where the file to be uploaded to
                                    $rule_attributes[1],                    //  should the original file name be preserved
                                    'block'     =>  $rule_attributes[2],    //  error block
                                    'message'   =>  $rule_attributes[3],    //  error message

                                );

                            break;

                    }

                }

            }

        }

        return $valid;

    }

    /**
     *  Generates a CSRF token, unique to the current form.
     *
     *  Note that this will generate a new CSRF token only when the form is generated and not also when the form is
     *  submitted - unless the <b>$force</b> argument is set to TRUE.
     *
     *  @param  boolean $force                  (Optional) Instructs the method to forcefully generate a new CSRF token.
     *
     *                                          This parameter will be TRUE when the method is called after an unsuccessful
     *                                          CSRF token validation or after a successful form validation.
     *
     *                                          By default, this method will generate a new CSRF token *only* if the form
     *                                          is not being currently submitted (form information is not available in the $_POST
     *                                          superglobal).
     *
     *                                          Default is FALSE.
     *
     *  @return void
     *
     *  @access private
     */
    function _csrf_generate_token($force = false)
    {

        // if CSRF protection is enabled (is not boolean FALSE) and CSRF token was not already generated
        if ($this->form_properties['csrf_storage_method'] !== false) {

            // reference to the form submission method
            global ${'_' . $this->form_properties['method']};

            $method = & ${'_' . $this->form_properties['method']};

            // if
            if (

                // form was submitted and we don't need to forcefully generate a new token
                isset($method[$this->form_properties['identifier']]) && $force === false &&
                // CSRF token is stored in a session variable
                $this->form_properties['csrf_storage_method'] == 'session' &&
                // the session variable exists
                isset($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                // the session variable holds an array
                is_array($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                // the array has 2 entries
                count($_SESSION[$this->form_properties['csrf_cookie_name']]) == 2

            // use the already existing CSRF token
            ) $this->form_properties['csrf_token'] = $_SESSION[$this->form_properties['csrf_cookie_name']][0];

            // else if
            elseif (

                // form was submitted and we don't need to forcefully generate a new token
                isset($method[$this->form_properties['identifier']]) && $force === false &&
                // CSRF token is stored in a cookie
                $this->form_properties['csrf_storage_method'] == 'cookie' &&
                // the cookie exists
                isset($_COOKIE[$this->form_properties['csrf_cookie_name']])

            // use the already existing CSRF token
            ) $this->form_properties['csrf_token'] = $_COOKIE[$this->form_properties['csrf_cookie_name']];

            // else, if form was not submitted, or we force new token generation
            elseif (!isset($method[$this->form_properties['identifier']])|| $force === true) {

                // generate a random token
                $this->form_properties['csrf_token'] = md5(uniqid(rand(), true));

                // compute token expiry timestamp
                $csrf_token_expiry = $this->form_properties['csrf_token_lifetime'] == 0 ? 0 : time() + $this->form_properties['csrf_token_lifetime'];

                // if storage method is "session"
                if ($this->form_properties['csrf_storage_method'] == 'session') {

                    // if no session is started, trigger an error message
                    if (!isset($_SESSION)) _zebra_form_show_error('You have chosen to enable protection against cross-site request forgery (CSRF) attacks and to use sessions for storing the CSRF token, but a session is not started! Start a session prior to calling the "csrf()" method', E_USER_ERROR);

                    // if sessions are on, store the CSRF token and the expiration data in session
                    $_SESSION[$this->form_properties['csrf_cookie_name']] = array($this->form_properties['csrf_token'], $csrf_token_expiry);

                // if storage method is "cookie"
                } else

                    // store the CSRF token in a cookie
                	setcookie(
                        $this->form_properties['csrf_cookie_name'],
                        $this->form_properties['csrf_token'],
                        $csrf_token_expiry,
                        $this->form_properties['csrf_cookie_config']['path'],
                        $this->form_properties['csrf_cookie_config']['domain'],
                        $this->form_properties['csrf_cookie_config']['secure'],
                        $this->form_properties['csrf_cookie_config']['httponly']
                    );

            }

        }

    }

    /**
     *  Validates CSRF token.
     *
     *  @return boolean     Returns TRUE if protection against CSRF attacks is disabled or it is enabled and the CSRF
     *                      token validates, or FALSE otherwise.
     *
     *  @access private
     */
    function _csrf_validate()
    {

        // if CSRF protection is enabled (is not boolean FALSE)
        if ($this->form_properties['csrf_storage_method'] !== false) {

            // reference to the form submission method
            global ${'_' . $this->form_properties['method']};

            $method = & ${'_' . $this->form_properties['method']};

            // if
            if (

                // the hidden field with the CSRF token was submitted
                isset($method[$this->form_properties['csrf_token_name']]) && (

                    // CSRF token is stored in a session variable
                    ($this->form_properties['csrf_storage_method'] == 'session' &&
                    // the session variable exists
                    isset($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                    // the session variable holds an array
                    is_array($_SESSION[$this->form_properties['csrf_cookie_name']]) &&
                    // the array has 2 entries
                    count($_SESSION[$this->form_properties['csrf_cookie_name']]) == 2 &&
                    // the value of the hidden field and the value in the session match
                    $method[$this->form_properties['csrf_token_name']] == $_SESSION[$this->form_properties['csrf_cookie_name']][0] &&
                    // if CSRF token doesn't expire or it does but it didn't yet
                    ($_SESSION[$this->form_properties['csrf_cookie_name']][1] == 0 || $_SESSION[$this->form_properties['csrf_cookie_name']][1] > time()))

                    ||

                    // CSRF token is stored in a cookie
                    ($this->form_properties['csrf_storage_method'] == 'cookie' &&
                    // the cookie exists
                    isset($_COOKIE[$this->form_properties['csrf_cookie_name']]) &&
                    // the value of the hidden field and the value in the cookie match
                    $method[$this->form_properties['csrf_token_name']] == $_COOKIE[$this->form_properties['csrf_cookie_name']])

                )

            // everything seems in order, then
            ) return true;

            // if we get here something was fishy...
            return false;

        }

        // if protection against CSRF attacks is not enabled, pretend nothing happened
        return true;

    }

    /**
     *  Converts an image from one type to another.
     *
     *  Note that this method will update the entries in the {@link $file_upload} property as the converted file will
     *  become the "uploaded" file!
     *
     *  @param  string  $control                The file upload control's name
     *
     *  @param  string  $type                   Type to convert an image to.
     *
     *                                          Can be (case-insensitive) JPG, PNG or GIF
     *
     *  @param  integer $jpeg_quality           (Optional) Indicates the quality of the output image (better quality
     *                                          means bigger file size).
     *
     *                                          Range is 0 - 100
     *
     *                                          Available only if <b>type</b> is "jpg".
     *
     *                                          Default is 85.
     *
     *  @param  integer $preserve_original_file (Optional) Should the original file be preserved after the conversion
     *                                          is done?
     *
     *                                          Default is FALSE.
     *
     *  @param  boolean $overwrite              (Optional) If a file with the same name as the converted file already
     *                                          exists, should it be overwritten or should the name be automatically
     *                                          computed.
     *
     *                                          If a file with the same name as the converted file already exists and
     *                                          this argument is FALSE, a suffix of "_n" (where n is an integer) will
     *                                          be appended to the file name.
     *
     *                                          Default is FALSE
     *
     *  @return boolean                         Returns TRUE on success or FALSE otherwise
     *
     *  @access private
     */
    function _convert($control, $type, $jpeg_quality = 85, $preserve_original_file = false, $overwrite = false)
    {

        // if
        if (

            // file was uploaded
            isset($this->file_upload[$control]) &&

            // and file is indeed an image file
            isset($this->file_upload[$control]['imageinfo']) &&

            // we're trying to convert to a supported file type
            ($type == 'gif' || $type == 'png' || $type == 'jpg')

        ) {

            // get file's current name
            $current_file_name = substr($this->file_upload[$control]['file_name'], 0, strrpos($this->file_upload[$control]['file_name'], '.'));

            // get file's current extension
            $current_file_extension = strtolower(substr($this->file_upload[$control]['file_name'], strrpos($this->file_upload[$control]['file_name'] + 1, '.')));

            // if extension is a variation of "jpeg", revert to default "jpg"
            if ($current_file_extension == 'jpeg') $current_file_extension = 'jpg';

            // make sure the new extension is also lowercase
            $type = strtolower($type);

            // if new extension is different than the file's current extension
            if ($type != $current_file_extension) {

                // if no overwrite and a file with the same name as the converted file already exists
                if (!$overwrite && is_file($this->file_upload[$control]['path'] . $current_file_name . '.' . $type)) {

                    $suffix = '';

                    // knowing the suffix...
                    // loop as long as
                    while (

                        // a file with the same name exists in the upload folder
                        // (file_exists returns also TRUE if a folder with that name exists)
                        is_file($this->file_upload[$control]['path'] . $current_file_name . $suffix . '.' . $type)

                    )

                        // if no suffix was yet set
                        if ($suffix === '')

                            // start the suffix like this
                            $suffix = '_1';

                        // if suffix was already initialized
                        else {

                            // drop the "_" from the suffix
                            $suffix = str_replace('_', '', $suffix);

                            // increment the suffix
                            $suffix = '_' . ++$suffix;

                        }

                    // the final file name
                    $current_file_name = $current_file_name . $suffix;

                }

                // if the image transformation class was not already instantiated
                if (!isset($this->Zebra_Image))

                    // create a new instance of the image transformation class
                    $this->Zebra_Image = new Zebra_Image();

                // set the source file
                $this->Zebra_Image->source_path = $this->file_upload[$control]['path'] . $this->file_upload[$control]['file_name'];

                // set the target file
                $this->Zebra_Image->target_path = $this->file_upload[$control]['path'] . $current_file_name . '.' . $type;

                // set the quality of the output image (better quality means bigger file size)
                // available only for jpeg files; ignored for other image types
                $this->Zebra_Image->jpeg_quality = $jpeg_quality;

                // if there was an error when resizing the image, return false
                if (!$this->Zebra_Image->resize(0, 0)) return false;

                // update entries in the file_upload property

                // get the size of the new file
                $this->file_upload[$control]['size'] = filesize($this->Zebra_Image->target_path);

                // update the file name (the file was converted and has a new extension)
                $this->file_upload[$control]['file_name'] = $current_file_name . '.' . $type;

                // get some info about the new file
                $imageinfo = @getimagesize($this->Zebra_Image->target_path);

                // rename some of the attributes returned by getimagesize
                $imageinfo['width'] = $imageinfo[0]; unset($imageinfo[0]);

                $imageinfo['height'] = $imageinfo[1]; unset($imageinfo[1]);

                $imageinfo['type'] = $imageinfo[2]; unset($imageinfo[2]);

                $imageinfo['html'] = $imageinfo[3]; unset($imageinfo[3]);

                // append image info to the file_upload property
                $this->file_upload[$control]['imageinfo'] = $imageinfo;

                // update the mime type as returned by getimagesize
                $this->file_upload[$control]['type'] = $imageinfo['mime'];

                // if original file is not to be preserved, delete original file
                if (!$preserve_original_file && !$overwrite) @unlink($this->Zebra_Image->source_path);

            }

        }

        // if the script gets this far, it means that everything went as planned and we return true
        return true;

    }

    /**
     *  Helper method for validating select boxes. It extract all the values from an infinitely nested array and puts
     *  them in an uni-dimensional array so that we can check if the submitted value is allowed.
     *
     *  @param  array   $array  The array to transform.
     *
     *  @return array           Returns the flat array.
     *
     *  @access private
     */
    function _extract_values($array)
    {

        $result = array();

        // iterate through the array's values
        foreach ($array as $index => $value)

            // if entry is an array, flatten array recursively
            if (is_array($value)) $result = array_merge($result, $this->_extract_values($value));

            // otherwise, add the index to the result array
            else $result[] = $index;

        // return found values
        return $result;

    }

    /**
     *  Resize an uploaded image
     *
     *  This method will do nothing if the file is not a supported image file.
     *
     *  @param  string  $control                The file upload control's name
     *
     *  @param  string  $prefix                 If the resized image is to be saved as a new file and the originally
     *                                          uploaded file needs to be preserved, specify a prefix to be used for the
     *                                          new file. This way, the resized image will have the same name as the
     *                                          original file but prefixed with the given value (i.e. "thumb_").
     *
     *                                          Specifying an empty string as argument will instruct the script to apply
     *                                          the resizing to the uploaded image and therefore overwriting the
     *                                          originally uploaded file.
     *
     *  @param  integer $width                  The width to resize the image to.
     *
     *                                          If set to <b>0</b>, the width will be automatically adjusted, depending
     *                                          on the value of the <b>height</b> argument so that the image preserves
     *                                          its aspect ratio.
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to TRUE and both this and the
     *                                          <b>height</b> arguments are values greater than <b>0</b>, the image will
     *                                          be resized to the exact required width and height and the aspect ratio
     *                                          will be preserved (see the description for the <b>method</b> argument
     *                                          below on how can this be done).
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to FALSE, the image will be
     *                                          resized to the required width and the aspect ratio will be ignored.
     *
     *                                          If both <b>width</b> and <b>height</b> are set to <b>0</b>, a copy of
     *                                          the source image will be created (<b>jpeg_quality</b> will still apply).
     *
     *                                          If either <b>width</b> or <b>height</b> are set to <b>0</b>, the script
     *                                          will consider the value of the {@link preserve_aspect_ratio} to bet set
     *                                          to TRUE regardless of its actual value!
     *
     *  @param  integer $height                 The height to resize the image to.
     *
     *                                          If set to <b>0</b>, the height will be automatically adjusted, depending
     *                                          on the value of the <b>width</b> argument so that the image preserves
     *                                          its aspect ratio.
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to TRUE and both this and the
     *                                          <b>width</b> arguments are values greater than <b>0</b>, the image will
     *                                          be resized to the exact required width and height and the aspect ratio
     *                                          will be preserved (see the description for the <b>method</b> argument
     *                                          below on how can this be done).
     *
     *                                          If <b>preserve_aspect_ratio</b> is set to FALSE, the image will be
     *                                          resized to the required height and the aspect ratio will be ignored.
     *
     *                                          If both <b>height</b> and <b>width</b> are set to <b>0</b>, a copy of
     *                                          the source image will be created (<b>jpeg_quality</b> will still apply).
     *
     *                                          If either <b>height</b> or <b>width</b> are set to <b>0</b>, the script
     *                                          will consider the value of the {@link preserve_aspect_ratio} to bet set
     *                                          to TRUE regardless of its actual value!
     *
     *  @param  boolean $preserve_aspect_ratio  (Optional) If set to TRUE, the image will be resized to the given width
     *                                          and height and the aspect ratio will be preserved.
     *
     *                                          Set this to FALSE if you want the image forcefully resized to the exact
     *                                          dimensions given by width and height ignoring the aspect ratio
     *
     *                                          Default is TRUE.
     *
     *  @param  int     $method                 (Optional) Method to use when resizing images to exact width and height
     *                                          while preserving aspect ratio.
     *
     *                                          If the $preserve_aspect_ratio property is set to TRUE and both the
     *                                          <b>width</b> and <b>height</b> arguments are values greater than <b>0</b>,
     *                                          the image will be resized to the exact given width and height and the
     *                                          aspect ratio will be preserved by using on of the following methods:
     *
     *                                          -   <b>ZEBRA_IMAGE_BOXED</b> - the image will be scalled so that it will
     *                                              fit in a box with the given width and height (both width/height will
     *                                              be smaller or equal to the required width/height) and then it will
     *                                              be centered both horizontally and vertically. The blank area will be
     *                                              filled with the color specified by the <b>$background_color</b>
     *                                              argument. (the blank area will be filled only if the image is not
     *                                              transparent!)
     *
     *                                          -   <b>ZEBRA_IMAGE_NOT_BOXED</b> - the image will be scalled so that it
     *                                              <i>could</i> fit in a box with the given width and height but will
     *                                              not be enclosed in a box with given width and height. The new width/
     *                                              height will be both smaller or equal to the required width/height
     *
     *                                          -   <b>ZEBRA_IMAGE_CROP_TOPLEFT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_TOPCENTER</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_TOPRIGHT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_MIDDLELEFT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_CENTER</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_MIDDLERIGHT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_BOTTOMLEFT</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_BOTTOMCENTER</b>
     *                                          -   <b>ZEBRA_IMAGE_CROP_BOTTOMRIGHT</b>
     *
     *                                          For the methods involving crop, first the image is scaled so that both
     *                                          its sides are equal or greater than the respective sizes of the bounding
     *                                          box; next, a region of required width and height will be cropped from
     *                                          indicated region of the resulted image.
     *
     *                                          Default is ZEBRA_IMAGE_BOXED
     *
     *  @param  boolean $background_color       (Optional) The hexadecimal color of the blank area (without the #).
     *                                          See the <b>method</b> argument.
     *
     *                                          Default is 'FFFFFF'
     *
     *  @param  boolean $enlarge_smaller_images (Optional) If set to FALSE, images having both width and height smaller
     *                                          than the required width and height, will be left untouched ({@link jpeg_quality}
     *                                          will still apply).
     *
     *                                          Default is TRUE
     *
     *  @param  boolean $quality                (Optional) Indicates the quality of the output image (better quality
     *                                          means bigger file size).
     *
     *                                          Range is 0 - 100
     *
     *                                          Available only for JPEG files.
     *
     *                                          Default is 85
     *
     *  @return boolean                         Returns TRUE on success or FALSE otherwise
     *
     *  @access private
     */
    function _resize($control, $prefix, $width, $height, $preserve_aspect_ratio = true, $method = ZEBRA_IMAGE_BOXED, $background_color = 'FFFFFF', $enlarge_smaller_images = true, $jpeg_quality = 85)
    {

        // if
        if (

            // file was uploaded
            isset($this->file_upload[$control]) &&

            // and file is indeed an image file
            isset($this->file_upload[$control]['imageinfo'])

        ) {

            // if the image transformation class was not already instantiated
            if (!isset($this->Zebra_Image))

                // create a new instance of the image transformation class
                $this->Zebra_Image = new Zebra_Image();

            // set the file permissions as per Zebra_Form's settings
            $this->Zebra_Image->chmod_value = $this->file_upload_permissions;

            // set the source file
            $this->Zebra_Image->source_path = $this->file_upload[$control]['path'] . $this->file_upload[$control]['file_name'];

            // set the target file
            $this->Zebra_Image->target_path = $this->file_upload[$control]['path'] . trim($prefix) . $this->file_upload[$control]['file_name'];

            // set whether aspect ratio should be maintained or not
            $this->Zebra_Image->maintain_ratio = $preserve_aspect_ratio;

            // set the quality of the output image (better quality means bigger file size)
            // available only for jpeg files; ignored for other image types
            $this->Zebra_Image->jpeg_quality = $jpeg_quality;

            // should smaller images be enlarged?
            $this->Zebra_Image->enlarge_smaller_images = $enlarge_smaller_images;

            // if there was an error when resizing the image, return false
            if (!$this->Zebra_Image->resize($width, $height, $method, $background_color)) return false;

        }

        // if the script gets this far, it means that everything went as planned and we return true
        return true;

    }

    /**
     *  Uploads a file
     *
     *  @param  string  $control                The file upload control's name
     *
     *  @param  string  $path                   The path where the file to be uploaded to
     *
     *  @param  boolean $filename               (Optional) Specifies whether the uploaded file's original name should be
     *                                          preserved, should it be prefixed with a string, or should it be randomly
     *                                          generated.
     *
     *                                          Possible values can be
     *
     *                                          -   TRUE - the uploaded file's original name will be preserved;
     *                                          -   FALSE (or, for better code readability, you should use the "ZEBRA_FORM_UPLOAD_RANDOM_NAMES"
     *                                              constant instead of "FALSE")- the uploaded file will have a randomly generated name;
     *                                          -   a string - the uploaded file's original name will be preserved but
     *                                              it will be prefixed with the given string (i.e. "original_", or "tmp_")
     *
     *                                          Note that when set to TRUE or a string, a suffix of "_n" (where n is an
     *                                          integer) will be appended to the file name if a file with the same name
     *                                          already exists at the given path.
     *
     *                                          Default is TRUE
     *
     *  @return boolean                         Returns TRUE on success or FALSE otherwise
     *
     *  @access private
     */
    function _upload($control, $folder, $filename = true)
    {

        // trim trailing slash from folder
        $folder = rtrim($folder, '\\/');

        // if upload folder does not have a trailing slash, add the trailing slash
        $folder = $folder . (substr($folder, -1) != DIRECTORY_SEPARATOR ? DIRECTORY_SEPARATOR : '');

        // if
        if (

            // the file upload control with the given name exists
            isset($_FILES[$control]) &&

            // file is ready to be uploaded
            $_FILES[$control]['error'] == 0 &&

            // the upload folder exists
            is_dir($folder)

        ) {

            // if file names should be random
            if ($filename === ZEBRA_FORM_UPLOAD_RANDOM_NAMES)

                // generate a random name for the file we're about to upload
                $file_name = md5(mt_rand() . microtime() . $_FILES[$control]['name']) . (strrpos($_FILES[$control]['name'], '.') !== false ? substr($_FILES[$control]['name'], strrpos($_FILES[$control]['name'], '.')) : '');

            // if file names are to be preserved
            else {

                // if the file we are about to upload does have an extension
                if (strrpos($_FILES[$control]['name'], '.') !== false) {

                    // split the file name into "file name"...
                    $file_name = substr($_FILES[$control]['name'], 0, strrpos($_FILES[$control]['name'], '.'));

                    // ...and "file extension"
                    $file_extension = substr($_FILES[$control]['name'], strrpos($_FILES[$control]['name'], '.'));

                // if the file we are about to upload does not have an extension
                } else {

                    // the file name will be the actual file name...
                    $file_name = $_FILES[$control]['name'];

                    // ...while the extension will be an empty string
                    $file_extension = '';

                }

                // prefix the file name if required
                $file_name = ($filename !== true ? $filename : '') . $file_name;

                $suffix = '';

                // knowing the suffix...
                // loop as long as
                while (

                    // a file with the same name exists in the upload folder
                    // (file_exists returns also TRUE if a folder with that name exists)
                    is_file($folder . $file_name . $suffix . $file_extension)

                ) {

                    // if no suffix was yet set
                    if ($suffix === '')

                        // start the suffix like this
                        $suffix = '_1';

                    // if suffix was already initialized
                    else {

                        // drop the "_" from the suffix
                        $suffix = str_replace('_', '', $suffix);

                        // increment the suffix
                        $suffix = '_' . ++$suffix;

                    }

                }

                // the final file name
                $file_name = $file_name . $suffix . $file_extension;

            }

            // if file could be uploaded
            if (@move_uploaded_file($_FILES[$control]['tmp_name'], $folder . $file_name)) {

                // get a list of functions disabled via configuration
                $disabled_functions = @ini_get('disable_functions');

                // if the 'chmod' function is not disabled via configuration
                if ($disabled_functions != '' && strpos('chmod', $disabled_functions) === false)

                    // chmod the file
                    chmod($folder . $file_name, intval($this->file_upload_permissions, 8));

                // set a special property
                // the value of the property will be an array will information about the uploaded file
                $this->file_upload[$control] = $_FILES[$control];

                $this->file_upload[$control]['path'] = $folder;

                $this->file_upload[$control]['file_name'] = $file_name;

                // if uploaded file is an image
                if ($imageinfo = @getimagesize($folder . $this->file_upload[$control]['file_name'])) {

                    // rename some of the attributes returned by getimagesize
                    $imageinfo['width'] = $imageinfo[0]; unset($imageinfo[0]);

                    $imageinfo['height'] = $imageinfo[1]; unset($imageinfo[1]);

                    $imageinfo['type'] = $imageinfo[2]; unset($imageinfo[2]);

                    $imageinfo['html'] = $imageinfo[3]; unset($imageinfo[3]);

                    // append image info to the file_upload property
                    $this->file_upload[$control]['imageinfo'] = $imageinfo;

                }

                // return true, as everything went as planned
                return true;

            }

        }

        // if script gets this far, return false as something must've gone wrong
        return false;

    }

}

/**
 *  A custom function for showing error messages in the Zebra_Form's environment.
 *
 *  We need this so we show correct file/line number information when reporting errors as PHP's trigger_error() shows the
 *  file and the line number where the function is called and it is not what we need here (we always trigger the errors
 *  from one of the Zebra_Form's file but the errors come from user files).
 *
 *  I didn't use a custom error handler so I don't interfere with the one you might be using.
 *
 *  @param  string  $message    The message to be shown to the user.
 *
 *  @param  mixed   $type       Severity of the error message.
 *
 *                              Can be E_USER_ERROR, E_USER_NOTICE or E_USER_WARNING.
 *
 *                              When set to E_USER_ERROR the execution of the script will be halted after the error
 *                              message is displayed.
 *
 *  @return void
 *
 *  @access private
 */
function _zebra_form_show_error($message, $type)
{

    // if error reporting is on
    if (($type & error_reporting()) == $type) {

        // get backtrace information
        $backtraceInfo = debug_backtrace();

        // this is where the error actually occurred
        $errorInfo = array_pop(array_slice($backtraceInfo, 2, 1));

        // show error message
        echo '<br><strong>' . ($type == E_USER_WARNING ? 'Warning' : ($type == E_USER_NOTICE ? 'Notice' : 'Fatal error')) . '</strong>:  ' . $message . ' in <strong>' . basename($errorInfo['file']) . '</strong> on line <strong>' . $errorInfo['line'] .  '</strong><br>';

        // die if necessary
        if ($type == E_USER_ERROR) die();

    }

}

?>